From 9986c9168456af263bd853bd35e2729be93757c6 Mon Sep 17 00:00:00 2001 From: crasm Date: Fri, 22 Dec 2023 01:23:46 -0500 Subject: [PATCH 01/11] python: add check-requirements.sh and GitHub workflow This script and workflow forces package versions to remain compatible across all convert*.py scripts, while allowing secondary convert scripts to import dependencies not wanted in convert.py. --- .../workflows/python-check-requirements.yml | 27 +++ check-requirements.sh | 157 ++++++++++++++++++ convert-persimmon-to-gguf.py | 1 + requirements-convert-hf-to-gguf.txt | 3 + requirements-convert-llama-ggml-to-gguf.txt | 1 + requirements-convert-lora-to-ggml.txt | 2 + requirements-convert-persimmon-to-gguf.txt | 2 + requirements-convert.txt | 5 + requirements.txt | 15 +- 9 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/python-check-requirements.yml create mode 100755 check-requirements.sh mode change 100644 => 100755 convert-persimmon-to-gguf.py create mode 100644 requirements-convert-hf-to-gguf.txt create mode 100644 requirements-convert-llama-ggml-to-gguf.txt create mode 100644 requirements-convert-lora-to-ggml.txt create mode 100644 requirements-convert-persimmon-to-gguf.txt create mode 100644 requirements-convert.txt diff --git a/.github/workflows/python-check-requirements.yml b/.github/workflows/python-check-requirements.yml new file mode 100644 index 0000000000000..cc97ee8100566 --- /dev/null +++ b/.github/workflows/python-check-requirements.yml @@ -0,0 +1,27 @@ +name: Python check requirements.txt + +on: + push: + paths: + - 'check-requirements.sh' + - 'convert*.py' + - 'requirements*.txt' + pull_request: + paths: + - 'check-requirements.sh' + - 'convert*.py' + - 'requirements*.txt' + +jobs: + python-check-requirements: + runs-on: ubuntu-latest + name: check-requirements + steps: + - name: Check out source repository + uses: actions/checkout@v3 + - name: Set up Python environment + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Run check-requirements.sh script + run: bash check-requirements.sh nocleanup diff --git a/check-requirements.sh b/check-requirements.sh new file mode 100755 index 0000000000000..a38e6293f1701 --- /dev/null +++ b/check-requirements.sh @@ -0,0 +1,157 @@ +#!/bin/bash +# +# check-requirements.sh checks all requirements files for each top-level +# convert*.py script. +# +# WARNING: This is quite IO intensive, because a fresh venv is set up for every +# python script. As of 2023-12-22, this writes ~2.7GB of data. An adequately +# sized tmpfs /tmp or ramdisk is recommended if running this frequently. +# +# usage: ./check-requirements.sh [] +# ./check-requirements.sh 'nocleanup' [] +# +# where: +# - is a directory that can be used as the base for +# setting up the venvs. Defaults to `/tmp`. +# - 'nocleanup' as the first argument will disable automatic cleanup +# of the files created by this script. +# +# requires: +# - bash >= 3.2.57 +# - shellcheck +# +# For each script, it creates a fresh venv, `pip install -r` the +# requirements, and finally executes the python script with no arguments to +# check for a `ModuleNotFoundError`. +# + +log() { + local level="$1"; shift + local format="$1"; shift + # shellcheck disable=SC2059 + >&2 printf "$level: $format\n" "$@" +} + +info() { + log 'INFO' "$@" +} + +fatal() { + log 'FATAL' "$@" + exit 1 +} + +cleanup() { + if [[ -n ${workdir+x} && -d $workdir && -w $workdir ]]; then + info "Removing $workdir" + ( + count=0 + rm -rfv "$workdir" | while read -r; do + if (( count++ > 750 )); then + printf '.' + count=0 + fi + done + printf '\n' + )& + wait $! + info "Removed '$workdir'" + fi +} + +abort() { + cleanup + exit 1 +} + +if [[ $1 == nocleanup ]]; then + shift # discard nocleanup arg +else + trap abort SIGINT SIGTERM SIGQUIT SIGABRT + trap cleanup EXIT +fi + +set -eu -o pipefail +this="$(realpath "$0")" +readonly this +cd "$(dirname "$this")" + +shellcheck "$this" + +workdir= +if [[ -n ${1+x} ]]; then + arg_dir="$(realpath "$1")" + if [[ ! ( -d $arg_dir && -w $arg_dir ) ]]; then + fatal "$arg_dir is not a valid directory" + fi + workdir="$(mktemp -d "$arg_dir/check-requirements.XXXX")" +else + workdir="$(mktemp -d "/tmp/check-requirements.XXXX")" +fi +readonly workdir + +info "Working directory: $workdir" + +assert_arg_count() { + local argcount="$1"; shift + if (( $# != argcount )); then + fatal "${FUNCNAME[1]}: incorrect number of args" + fi +} + +check_requirements() { + assert_arg_count 2 "$@" + local venv="$1" + local reqs="$2" + + info "$reqs: beginning check" + ( + # shellcheck source=/dev/null + source "$venv/bin/activate" + pip --disable-pip-version-check install -q -r "$reqs" + ) + info "$reqs: OK" +} + +check_convert_script() { + assert_arg_count 1 "$@" + local py="$1" + local pyname="${py%.py}" + + info "$py: beginning check" + + local reqs="requirements-$pyname.txt" + if [[ ! -r "$reqs" ]]; then + fatal "$py missing requirements. Expected: $reqs" + fi + + local venv="$workdir/$pyname-venv" + python3 -m venv "$venv" + + check_requirements "$venv" "$reqs" + set +e + ( + # shellcheck source=/dev/null + source "$venv/bin/activate" + py_err="$workdir/$pyname.out" + python "$py" 2> "$py_err" + >&2 cat "$py_err" + grep -e 'ModuleNotFoundError' "$py_err" + ) + set -e + # shellcheck disable=SC2181 + (( $? )) && fatal "$py: some imports not declared in $reqs" + info "$py: imports OK" +} + +# Check requirements.txt +all_venv="$workdir/all-venv" +python3 -m venv "$all_venv" +check_requirements "$all_venv" 'requirements.txt' + +check_convert_script 'convert.py' +for py in convert-*.py; do + check_convert_script "$py" +done + +info "Done! No issues found." diff --git a/convert-persimmon-to-gguf.py b/convert-persimmon-to-gguf.py old mode 100644 new mode 100755 index 206b7d5ff9e31..1ba5864dc25ec --- a/convert-persimmon-to-gguf.py +++ b/convert-persimmon-to-gguf.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 import torch import os from pprint import pprint diff --git a/requirements-convert-hf-to-gguf.txt b/requirements-convert-hf-to-gguf.txt new file mode 100644 index 0000000000000..4d00b19666108 --- /dev/null +++ b/requirements-convert-hf-to-gguf.txt @@ -0,0 +1,3 @@ +-r requirements-convert.txt +torch==2.1.1 +transformers==4.35.2 diff --git a/requirements-convert-llama-ggml-to-gguf.txt b/requirements-convert-llama-ggml-to-gguf.txt new file mode 100644 index 0000000000000..8a5377762c1fa --- /dev/null +++ b/requirements-convert-llama-ggml-to-gguf.txt @@ -0,0 +1 @@ +-r requirements-convert.txt diff --git a/requirements-convert-lora-to-ggml.txt b/requirements-convert-lora-to-ggml.txt new file mode 100644 index 0000000000000..30827c8964d3e --- /dev/null +++ b/requirements-convert-lora-to-ggml.txt @@ -0,0 +1,2 @@ +-r requirements-convert.txt +torch==2.1.1 diff --git a/requirements-convert-persimmon-to-gguf.txt b/requirements-convert-persimmon-to-gguf.txt new file mode 100644 index 0000000000000..30827c8964d3e --- /dev/null +++ b/requirements-convert-persimmon-to-gguf.txt @@ -0,0 +1,2 @@ +-r requirements-convert.txt +torch==2.1.1 diff --git a/requirements-convert.txt b/requirements-convert.txt new file mode 100644 index 0000000000000..1a116256671e5 --- /dev/null +++ b/requirements-convert.txt @@ -0,0 +1,5 @@ +numpy==1.24.4 +sentencepiece==0.1.98 +transformers>=4.34.0 +gguf>=0.1.0 +protobuf>=4.21.0 diff --git a/requirements.txt b/requirements.txt index badfec3be804c..da4f3f9a874e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,11 @@ -numpy==1.24.4 -sentencepiece==0.1.98 -transformers>=4.34.0 -gguf>=0.1.0 +# These requirements include all dependencies for all top-level python scripts +# for llama.cpp. Avoid adding packages here directly. +# +# Package versions must stay compatible across all top-level python scripts. +# + +-r requirements-convert.txt + +-r requirements-convert-hf-to-gguf.txt +-r requirements-convert-lora-to-ggml.txt +-r requirements-convert-persimmon-to-gguf.txt From b6a9efb0e04b1d76b48fa875c2decea3f20536c1 Mon Sep 17 00:00:00 2001 From: crasm Date: Sat, 23 Dec 2023 17:18:21 -0500 Subject: [PATCH 02/11] Move requirements into ./requirements --- check-requirements.sh | 30 +++++++++++++------ requirements-convert-llama-ggml-to-gguf.txt | 1 - requirements-convert-lora-to-ggml.txt | 2 -- requirements-convert-persimmon-to-gguf.txt | 2 -- requirements-hf-to-gguf.txt | 3 -- requirements.txt | 9 +++--- .../requirements-convert-hf-to-gguf.txt | 2 +- ...equirements-convert-llama-ggml-to-gguf.txt | 1 + .../requirements-convert-lora-to-ggml.txt | 2 ++ ...requirements-convert-persimmon-to-gguf.txt | 2 ++ .../requirements-convert.txt | 0 11 files changed, 32 insertions(+), 22 deletions(-) delete mode 100644 requirements-convert-llama-ggml-to-gguf.txt delete mode 100644 requirements-convert-lora-to-ggml.txt delete mode 100644 requirements-convert-persimmon-to-gguf.txt delete mode 100644 requirements-hf-to-gguf.txt rename requirements-convert-hf-to-gguf.txt => requirements/requirements-convert-hf-to-gguf.txt (53%) create mode 100644 requirements/requirements-convert-llama-ggml-to-gguf.txt create mode 100644 requirements/requirements-convert-lora-to-ggml.txt create mode 100644 requirements/requirements-convert-persimmon-to-gguf.txt rename requirements-convert.txt => requirements/requirements-convert.txt (100%) diff --git a/check-requirements.sh b/check-requirements.sh index a38e6293f1701..b1f8ac31391d6 100755 --- a/check-requirements.sh +++ b/check-requirements.sh @@ -32,6 +32,10 @@ log() { >&2 printf "$level: $format\n" "$@" } +debug () { + log 'DEBUG' "$@" +} + info() { log 'INFO' "$@" } @@ -72,12 +76,13 @@ else fi set -eu -o pipefail -this="$(realpath "$0")" -readonly this +this="$(realpath "$0")"; readonly this cd "$(dirname "$this")" shellcheck "$this" +readonly reqs_dir='./requirements' + workdir= if [[ -n ${1+x} ]]; then arg_dir="$(realpath "$1")" @@ -115,12 +120,13 @@ check_requirements() { check_convert_script() { assert_arg_count 1 "$@" - local py="$1" - local pyname="${py%.py}" + local py="$1"; shift # e.g. ./convert-hf-to-gguf.py + local pyname; pyname="$(basename "$py")" # e.g. convert-hf-to-gguf.py + pyname="${pyname%.py}" # e.g. convert-hf-to-gguf info "$py: beginning check" - local reqs="requirements-$pyname.txt" + local reqs="$reqs_dir/requirements-$pyname.txt" if [[ ! -r "$reqs" ]]; then fatal "$py missing requirements. Expected: $reqs" fi @@ -144,13 +150,19 @@ check_convert_script() { info "$py: imports OK" } -# Check requirements.txt +# Check that all sub-requirements are added to top-level requirements.txt +for req in "$reqs_dir"/*; do + if ! grep -qFe "$req" ./requirements.txt; then + fatal "$req needs to be added to ./requirements.txt" + fi +done + all_venv="$workdir/all-venv" python3 -m venv "$all_venv" -check_requirements "$all_venv" 'requirements.txt' +check_requirements "$all_venv" './requirements.txt' -check_convert_script 'convert.py' -for py in convert-*.py; do +check_convert_script './convert.py' +for py in ./convert-*.py;do check_convert_script "$py" done diff --git a/requirements-convert-llama-ggml-to-gguf.txt b/requirements-convert-llama-ggml-to-gguf.txt deleted file mode 100644 index 8a5377762c1fa..0000000000000 --- a/requirements-convert-llama-ggml-to-gguf.txt +++ /dev/null @@ -1 +0,0 @@ --r requirements-convert.txt diff --git a/requirements-convert-lora-to-ggml.txt b/requirements-convert-lora-to-ggml.txt deleted file mode 100644 index 30827c8964d3e..0000000000000 --- a/requirements-convert-lora-to-ggml.txt +++ /dev/null @@ -1,2 +0,0 @@ --r requirements-convert.txt -torch==2.1.1 diff --git a/requirements-convert-persimmon-to-gguf.txt b/requirements-convert-persimmon-to-gguf.txt deleted file mode 100644 index 30827c8964d3e..0000000000000 --- a/requirements-convert-persimmon-to-gguf.txt +++ /dev/null @@ -1,2 +0,0 @@ --r requirements-convert.txt -torch==2.1.1 diff --git a/requirements-hf-to-gguf.txt b/requirements-hf-to-gguf.txt deleted file mode 100644 index f4600539e27ac..0000000000000 --- a/requirements-hf-to-gguf.txt +++ /dev/null @@ -1,3 +0,0 @@ --r requirements.txt -torch==2.1.1 -transformers==4.35.2 diff --git a/requirements.txt b/requirements.txt index da4f3f9a874e3..d36f745201ef0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,8 +4,9 @@ # Package versions must stay compatible across all top-level python scripts. # --r requirements-convert.txt +-r ./requirements/requirements-convert.txt --r requirements-convert-hf-to-gguf.txt --r requirements-convert-lora-to-ggml.txt --r requirements-convert-persimmon-to-gguf.txt +-r ./requirements/requirements-convert-hf-to-gguf.txt +-r ./requirements/requirements-convert-llama-ggml-to-gguf.txt +-r ./requirements/requirements-convert-lora-to-ggml.txt +-r ./requirements/requirements-convert-persimmon-to-gguf.txt diff --git a/requirements-convert-hf-to-gguf.txt b/requirements/requirements-convert-hf-to-gguf.txt similarity index 53% rename from requirements-convert-hf-to-gguf.txt rename to requirements/requirements-convert-hf-to-gguf.txt index 4d00b19666108..ddde1431bc642 100644 --- a/requirements-convert-hf-to-gguf.txt +++ b/requirements/requirements-convert-hf-to-gguf.txt @@ -1,3 +1,3 @@ --r requirements-convert.txt +-r ./requirements-convert.txt torch==2.1.1 transformers==4.35.2 diff --git a/requirements/requirements-convert-llama-ggml-to-gguf.txt b/requirements/requirements-convert-llama-ggml-to-gguf.txt new file mode 100644 index 0000000000000..a0f37cd1c71e4 --- /dev/null +++ b/requirements/requirements-convert-llama-ggml-to-gguf.txt @@ -0,0 +1 @@ +-r ./requirements-convert.txt diff --git a/requirements/requirements-convert-lora-to-ggml.txt b/requirements/requirements-convert-lora-to-ggml.txt new file mode 100644 index 0000000000000..0605b9816c136 --- /dev/null +++ b/requirements/requirements-convert-lora-to-ggml.txt @@ -0,0 +1,2 @@ +-r ./requirements-convert.txt +torch==2.1.1 diff --git a/requirements/requirements-convert-persimmon-to-gguf.txt b/requirements/requirements-convert-persimmon-to-gguf.txt new file mode 100644 index 0000000000000..0605b9816c136 --- /dev/null +++ b/requirements/requirements-convert-persimmon-to-gguf.txt @@ -0,0 +1,2 @@ +-r ./requirements-convert.txt +torch==2.1.1 diff --git a/requirements-convert.txt b/requirements/requirements-convert.txt similarity index 100% rename from requirements-convert.txt rename to requirements/requirements-convert.txt From dc209c09a85906ebbfb9e01d503f814cd3d73bc9 Mon Sep 17 00:00:00 2001 From: crasm Date: Sat, 23 Dec 2023 18:06:24 -0500 Subject: [PATCH 03/11] Fail on "==" being used for package requirements (but can be suppressed) --- check-requirements.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/check-requirements.sh b/check-requirements.sh index b1f8ac31391d6..50a8dad0349dc 100755 --- a/check-requirements.sh +++ b/check-requirements.sh @@ -150,11 +150,20 @@ check_convert_script() { info "$py: imports OK" } -# Check that all sub-requirements are added to top-level requirements.txt +readonly ignore_eq_eq='check_requirements: ignore "=="' + for req in "$reqs_dir"/*; do + # Check that all sub-requirements are added to top-level requirements.txt if ! grep -qFe "$req" ./requirements.txt; then fatal "$req needs to be added to ./requirements.txt" fi + + # Make sure exact release versions aren't being pinned in the requirements + # Filters out the ignore string + req_no_ignore_eq_eq="$(grep -vF "$ignore_eq_eq" "$req")" + if grep -Fe '==' <<< "$req_no_ignore_eq_eq" ; then + fatal "Avoid pinning exact package versions. Use '=~' instead.\nYou can suppress this error by appending the following to the line: \n\t# $ignore_eq_eq" + fi done all_venv="$workdir/all-venv" From 91f318c228c233cb7737b468c49c701fac3b6b07 Mon Sep 17 00:00:00 2001 From: crasm Date: Sat, 23 Dec 2023 18:27:43 -0500 Subject: [PATCH 04/11] Enforce "compatible release" syntax instead of == --- check-requirements.sh | 20 ++++++++++--------- .../requirements-convert-hf-to-gguf.txt | 4 ++-- .../requirements-convert-lora-to-ggml.txt | 2 +- ...requirements-convert-persimmon-to-gguf.txt | 2 +- requirements/requirements-convert.txt | 4 ++-- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/check-requirements.sh b/check-requirements.sh index 50a8dad0349dc..ac23b46cb7100 100755 --- a/check-requirements.sh +++ b/check-requirements.sh @@ -135,18 +135,20 @@ check_convert_script() { python3 -m venv "$venv" check_requirements "$venv" "$reqs" - set +e - ( + + # Because we mask the return value of the subshell, + # we don't need to use set +e/-e. + # shellcheck disable=SC2155 + local py_err=$( # shellcheck source=/dev/null source "$venv/bin/activate" - py_err="$workdir/$pyname.out" - python "$py" 2> "$py_err" - >&2 cat "$py_err" - grep -e 'ModuleNotFoundError' "$py_err" + python "$py" 2>&1 ) - set -e + # shellcheck disable=SC2181 - (( $? )) && fatal "$py: some imports not declared in $reqs" + if grep -Fe 'ModuleNotFoundError' <<< "$py_err"; then + fatal "$py: some imports not declared in $reqs" + fi info "$py: imports OK" } @@ -162,7 +164,7 @@ for req in "$reqs_dir"/*; do # Filters out the ignore string req_no_ignore_eq_eq="$(grep -vF "$ignore_eq_eq" "$req")" if grep -Fe '==' <<< "$req_no_ignore_eq_eq" ; then - fatal "Avoid pinning exact package versions. Use '=~' instead.\nYou can suppress this error by appending the following to the line: \n\t# $ignore_eq_eq" + fatal "Avoid pinning exact package versions. Use '~=' instead.\nYou can suppress this error by appending the following to the line: \n\t# $ignore_eq_eq" fi done diff --git a/requirements/requirements-convert-hf-to-gguf.txt b/requirements/requirements-convert-hf-to-gguf.txt index ddde1431bc642..0d16e2da90ad9 100644 --- a/requirements/requirements-convert-hf-to-gguf.txt +++ b/requirements/requirements-convert-hf-to-gguf.txt @@ -1,3 +1,3 @@ -r ./requirements-convert.txt -torch==2.1.1 -transformers==4.35.2 +torch~=2.1.1 +transformers~=4.35.2 diff --git a/requirements/requirements-convert-lora-to-ggml.txt b/requirements/requirements-convert-lora-to-ggml.txt index 0605b9816c136..6ac4026107fbe 100644 --- a/requirements/requirements-convert-lora-to-ggml.txt +++ b/requirements/requirements-convert-lora-to-ggml.txt @@ -1,2 +1,2 @@ -r ./requirements-convert.txt -torch==2.1.1 +torch~=2.1.1 diff --git a/requirements/requirements-convert-persimmon-to-gguf.txt b/requirements/requirements-convert-persimmon-to-gguf.txt index 0605b9816c136..6ac4026107fbe 100644 --- a/requirements/requirements-convert-persimmon-to-gguf.txt +++ b/requirements/requirements-convert-persimmon-to-gguf.txt @@ -1,2 +1,2 @@ -r ./requirements-convert.txt -torch==2.1.1 +torch~=2.1.1 diff --git a/requirements/requirements-convert.txt b/requirements/requirements-convert.txt index 1a116256671e5..09311906f8438 100644 --- a/requirements/requirements-convert.txt +++ b/requirements/requirements-convert.txt @@ -1,5 +1,5 @@ -numpy==1.24.4 -sentencepiece==0.1.98 +numpy~=1.24.4 +sentencepiece~=0.1.98 transformers>=4.34.0 gguf>=0.1.0 protobuf>=4.21.0 From 16ad7f74ee7c31d49e1bff626df9ebcbe4b3bc69 Mon Sep 17 00:00:00 2001 From: crasm Date: Sat, 23 Dec 2023 18:55:55 -0500 Subject: [PATCH 05/11] Update workflow --- .github/workflows/python-check-requirements.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-check-requirements.yml b/.github/workflows/python-check-requirements.yml index cc97ee8100566..6a27c618ff9c1 100644 --- a/.github/workflows/python-check-requirements.yml +++ b/.github/workflows/python-check-requirements.yml @@ -5,12 +5,14 @@ on: paths: - 'check-requirements.sh' - 'convert*.py' - - 'requirements*.txt' + - 'requirements.txt' + - 'requirements/*.txt' pull_request: paths: - 'check-requirements.sh' - 'convert*.py' - - 'requirements*.txt' + - 'requirements.txt' + - 'requirements/*.txt' jobs: python-check-requirements: From cb587757194fc3eba89be37f19f898fddd6bd765 Mon Sep 17 00:00:00 2001 From: crasm Date: Wed, 27 Dec 2023 02:09:30 -0500 Subject: [PATCH 06/11] Add upper version bound for transformers and protobuf --- requirements/requirements-convert-hf-to-gguf.txt | 1 - requirements/requirements-convert.txt | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements/requirements-convert-hf-to-gguf.txt b/requirements/requirements-convert-hf-to-gguf.txt index 0d16e2da90ad9..6ac4026107fbe 100644 --- a/requirements/requirements-convert-hf-to-gguf.txt +++ b/requirements/requirements-convert-hf-to-gguf.txt @@ -1,3 +1,2 @@ -r ./requirements-convert.txt torch~=2.1.1 -transformers~=4.35.2 diff --git a/requirements/requirements-convert.txt b/requirements/requirements-convert.txt index 09311906f8438..a3d6ecec0ac04 100644 --- a/requirements/requirements-convert.txt +++ b/requirements/requirements-convert.txt @@ -1,5 +1,5 @@ numpy~=1.24.4 sentencepiece~=0.1.98 -transformers>=4.34.0 +transformers>=4.35.2,<5.0.0 gguf>=0.1.0 -protobuf>=4.21.0 +protobuf>=4.21.0,<5.0.0 From ce26f49208ca027f4ff70bdf7adb176665fdeabe Mon Sep 17 00:00:00 2001 From: Jared Van Bortel Date: Wed, 27 Dec 2023 21:27:18 -0500 Subject: [PATCH 07/11] improve check-requirements.sh --- check-requirements.sh | 152 +++++++++++++++------------------ convert-hf-to-gguf.py | 71 ++++++++-------- convert-lora-to-ggml.py | 183 ++++++++++++++++++++-------------------- 3 files changed, 199 insertions(+), 207 deletions(-) diff --git a/check-requirements.sh b/check-requirements.sh index ac23b46cb7100..77ed98fcb1f22 100755 --- a/check-requirements.sh +++ b/check-requirements.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -euo pipefail # # check-requirements.sh checks all requirements files for each top-level # convert*.py script. @@ -8,7 +9,7 @@ # sized tmpfs /tmp or ramdisk is recommended if running this frequently. # # usage: ./check-requirements.sh [] -# ./check-requirements.sh 'nocleanup' [] +# ./check-requirements.sh nocleanup [] # # where: # - is a directory that can be used as the base for @@ -20,135 +21,108 @@ # - bash >= 3.2.57 # - shellcheck # -# For each script, it creates a fresh venv, `pip install -r` the -# requirements, and finally executes the python script with no arguments to -# check for a `ModuleNotFoundError`. +# For each script, it creates a fresh venv, `pip install`s the requirements, and +# finally imports the python script to check for `ImportError`. # log() { - local level="$1"; shift - local format="$1"; shift - # shellcheck disable=SC2059 - >&2 printf "$level: $format\n" "$@" + local level=$1 msg=$2 + printf >&2 '%s: %s\n' "$level" "$msg" } -debug () { - log 'DEBUG' "$@" +debug() { + log DEBUG "$@" } info() { - log 'INFO' "$@" + log INFO "$@" } fatal() { - log 'FATAL' "$@" + log FATAL "$@" exit 1 } cleanup() { if [[ -n ${workdir+x} && -d $workdir && -w $workdir ]]; then info "Removing $workdir" - ( - count=0 - rm -rfv "$workdir" | while read -r; do - if (( count++ > 750 )); then - printf '.' - count=0 - fi - done - printf '\n' - )& - wait $! - info "Removed '$workdir'" + local count=0 + rm -rfv -- "$workdir" | while read -r; do + if (( count++ > 750 )); then + printf . + count=0 + fi + done + printf '\n' + info "Removed $workdir" fi } -abort() { - cleanup - exit 1 -} - -if [[ $1 == nocleanup ]]; then - shift # discard nocleanup arg +if [[ ${1-} == nocleanup ]]; then + shift # discard nocleanup arg else - trap abort SIGINT SIGTERM SIGQUIT SIGABRT + trap exit INT TERM trap cleanup EXIT fi -set -eu -o pipefail -this="$(realpath "$0")"; readonly this +this=$(realpath -- "$0"); readonly this cd "$(dirname "$this")" shellcheck "$this" -readonly reqs_dir='./requirements' +readonly reqs_dir=requirements -workdir= -if [[ -n ${1+x} ]]; then - arg_dir="$(realpath "$1")" - if [[ ! ( -d $arg_dir && -w $arg_dir ) ]]; then - fatal "$arg_dir is not a valid directory" +if [[ ${1+x} ]]; then + tmp_dir=$(realpath -- "$1") + if [[ ! ( -d $tmp_dir && -w $tmp_dir ) ]]; then + fatal "$tmp_dir is not a writable directory" fi - workdir="$(mktemp -d "$arg_dir/check-requirements.XXXX")" else - workdir="$(mktemp -d "/tmp/check-requirements.XXXX")" + tmp_dir=/tmp fi -readonly workdir +workdir=$(mktemp -d "$tmp_dir/check-requirements.XXXX"); readonly workdir info "Working directory: $workdir" -assert_arg_count() { - local argcount="$1"; shift - if (( $# != argcount )); then - fatal "${FUNCNAME[1]}: incorrect number of args" - fi -} - check_requirements() { - assert_arg_count 2 "$@" - local venv="$1" - local reqs="$2" + local reqs=$1 info "$reqs: beginning check" - ( - # shellcheck source=/dev/null - source "$venv/bin/activate" - pip --disable-pip-version-check install -q -r "$reqs" - ) + pip --disable-pip-version-check install -qr "$reqs" info "$reqs: OK" } check_convert_script() { - assert_arg_count 1 "$@" - local py="$1"; shift # e.g. ./convert-hf-to-gguf.py - local pyname; pyname="$(basename "$py")" # e.g. convert-hf-to-gguf.py - pyname="${pyname%.py}" # e.g. convert-hf-to-gguf + local py=$1 # e.g. ./convert-hf-to-gguf.py + local pyname=${py##*/} # e.g. convert-hf-to-gguf.py + pyname=${pyname%.py} # e.g. convert-hf-to-gguf info "$py: beginning check" local reqs="$reqs_dir/requirements-$pyname.txt" - if [[ ! -r "$reqs" ]]; then + if [[ ! -r $reqs ]]; then fatal "$py missing requirements. Expected: $reqs" fi local venv="$workdir/$pyname-venv" python3 -m venv "$venv" - check_requirements "$venv" "$reqs" - - # Because we mask the return value of the subshell, - # we don't need to use set +e/-e. - # shellcheck disable=SC2155 - local py_err=$( + ( # shellcheck source=/dev/null source "$venv/bin/activate" - python "$py" 2>&1 + + check_requirements "$reqs" + + python - "$py" "$pyname" <&2 < argparse.Namespace: return parser.parse_args() -args = parse_args() +def main() -> None: + args = parse_args() -dir_model = args.model -if not dir_model.is_dir(): - print(f'Error: {args.model} is not a directory', file=sys.stderr) - sys.exit(1) + dir_model = args.model + if not dir_model.is_dir(): + print(f'Error: {args.model} is not a directory', file=sys.stderr) + sys.exit(1) -ftype_map = { - "f32": gguf.GGMLQuantizationType.F32, - "f16": gguf.GGMLQuantizationType.F16, -} + ftype_map = { + "f32": gguf.GGMLQuantizationType.F32, + "f16": gguf.GGMLQuantizationType.F16, + } -if args.outfile is not None: - fname_out = args.outfile -else: - # output in the same directory as the model by default - fname_out = dir_model / f'ggml-model-{args.outtype}.gguf' + if args.outfile is not None: + fname_out = args.outfile + else: + # output in the same directory as the model by default + fname_out = dir_model / f'ggml-model-{args.outtype}.gguf' -print(f"Loading model: {dir_model.name}") + print(f"Loading model: {dir_model.name}") -hparams = Model.load_hparams(dir_model) + hparams = Model.load_hparams(dir_model) -with torch.inference_mode(): - model_class = Model.from_model_architecture(hparams["architectures"][0]) - model_instance = model_class(dir_model, ftype_map[args.outtype], fname_out, args.bigendian) + with torch.inference_mode(): + model_class = Model.from_model_architecture(hparams["architectures"][0]) + model_instance = model_class(dir_model, ftype_map[args.outtype], fname_out, args.bigendian) - print("Set model parameters") - model_instance.set_gguf_parameters() + print("Set model parameters") + model_instance.set_gguf_parameters() - print("Set model tokenizer") - model_instance.set_vocab() + print("Set model tokenizer") + model_instance.set_vocab() + + if args.vocab_only: + print(f"Exporting model vocab to '{fname_out}'") + model_instance.write_vocab() + else: + print(f"Exporting model to '{fname_out}'") + model_instance.write() + + print(f"Model successfully exported to '{fname_out}'") - if args.vocab_only: - print(f"Exporting model vocab to '{fname_out}'") - model_instance.write_vocab() - else: - print(f"Exporting model to '{fname_out}'") - model_instance.write() - print(f"Model successfully exported to '{fname_out}'") +if __name__ == '__main__': + main() diff --git a/convert-lora-to-ggml.py b/convert-lora-to-ggml.py index 53bb8a3d97a05..35ce152f4248d 100755 --- a/convert-lora-to-ggml.py +++ b/convert-lora-to-ggml.py @@ -47,95 +47,96 @@ def write_tensor_header(fout: BinaryIO, name: str, shape: Sequence[int], data_ty fout.seek((fout.tell() + 31) & -32) -if len(sys.argv) < 2: - print(f"Usage: python {sys.argv[0]} [arch]") - print( - "Path must contain HuggingFace PEFT LoRA files 'adapter_config.json' and 'adapter_model.bin'" - ) - print(f"Arch must be one of {list(gguf.MODEL_ARCH_NAMES.values())} (default: llama)") - sys.exit(1) - -input_json = os.path.join(sys.argv[1], "adapter_config.json") -input_model = os.path.join(sys.argv[1], "adapter_model.bin") -output_path = os.path.join(sys.argv[1], "ggml-adapter-model.bin") - -model = torch.load(input_model, map_location="cpu") -arch_name = sys.argv[2] if len(sys.argv) == 3 else "llama" - -if arch_name not in gguf.MODEL_ARCH_NAMES.values(): - print(f"Error: unsupported architecture {arch_name}") - sys.exit(1) - -arch = list(gguf.MODEL_ARCH_NAMES.keys())[list(gguf.MODEL_ARCH_NAMES.values()).index(arch_name)] -name_map = gguf.TensorNameMap(arch, 200) # 200 layers ought to be enough for anyone - -with open(input_json, "r") as f: - params = json.load(f) - -if params["peft_type"] != "LORA": - print(f"Error: unsupported adapter type {params['peft_type']}, expected LORA") - sys.exit(1) - -if params["fan_in_fan_out"] is True: - print("Error: param fan_in_fan_out is not supported") - sys.exit(1) - -if params["bias"] is not None and params["bias"] != "none": - print("Error: param bias is not supported") - sys.exit(1) - -# TODO: these seem to be layers that have been trained but without lora. -# doesn't seem widely used but eventually should be supported -if params["modules_to_save"] is not None and len(params["modules_to_save"]) > 0: - print("Error: param modules_to_save is not supported") - sys.exit(1) - -with open(output_path, "wb") as fout: - fout.truncate() - - write_file_header(fout, params) - for k, v in model.items(): - orig_k = k - if k.endswith(".default.weight"): - k = k.replace(".default.weight", ".weight") - if k in ["llama_proj.weight", "llama_proj.bias"]: - continue - if k.endswith("lora_A.weight"): - if v.dtype != torch.float16 and v.dtype != torch.float32: +if __name__ == '__main__': + if len(sys.argv) < 2: + print(f"Usage: python {sys.argv[0]} [arch]") + print( + "Path must contain HuggingFace PEFT LoRA files 'adapter_config.json' and 'adapter_model.bin'" + ) + print(f"Arch must be one of {list(gguf.MODEL_ARCH_NAMES.values())} (default: llama)") + sys.exit(1) + + input_json = os.path.join(sys.argv[1], "adapter_config.json") + input_model = os.path.join(sys.argv[1], "adapter_model.bin") + output_path = os.path.join(sys.argv[1], "ggml-adapter-model.bin") + + model = torch.load(input_model, map_location="cpu") + arch_name = sys.argv[2] if len(sys.argv) == 3 else "llama" + + if arch_name not in gguf.MODEL_ARCH_NAMES.values(): + print(f"Error: unsupported architecture {arch_name}") + sys.exit(1) + + arch = list(gguf.MODEL_ARCH_NAMES.keys())[list(gguf.MODEL_ARCH_NAMES.values()).index(arch_name)] + name_map = gguf.TensorNameMap(arch, 200) # 200 layers ought to be enough for anyone + + with open(input_json, "r") as f: + params = json.load(f) + + if params["peft_type"] != "LORA": + print(f"Error: unsupported adapter type {params['peft_type']}, expected LORA") + sys.exit(1) + + if params["fan_in_fan_out"] is True: + print("Error: param fan_in_fan_out is not supported") + sys.exit(1) + + if params["bias"] is not None and params["bias"] != "none": + print("Error: param bias is not supported") + sys.exit(1) + + # TODO: these seem to be layers that have been trained but without lora. + # doesn't seem widely used but eventually should be supported + if params["modules_to_save"] is not None and len(params["modules_to_save"]) > 0: + print("Error: param modules_to_save is not supported") + sys.exit(1) + + with open(output_path, "wb") as fout: + fout.truncate() + + write_file_header(fout, params) + for k, v in model.items(): + orig_k = k + if k.endswith(".default.weight"): + k = k.replace(".default.weight", ".weight") + if k in ["llama_proj.weight", "llama_proj.bias"]: + continue + if k.endswith("lora_A.weight"): + if v.dtype != torch.float16 and v.dtype != torch.float32: + v = v.float() + v = v.T + else: v = v.float() - v = v.T - else: - v = v.float() - - t = v.detach().numpy() - - prefix = "base_model.model." - if k.startswith(prefix): - k = k[len(prefix) :] - - lora_suffixes = (".lora_A.weight", ".lora_B.weight") - if k.endswith(lora_suffixes): - suffix = k[-len(lora_suffixes[0]):] - k = k[: -len(lora_suffixes[0])] - else: - print(f"Error: unrecognized tensor name {orig_k}") - sys.exit(1) - - tname = name_map.get_name(k) - if tname is None: - print(f"Error: could not map tensor name {orig_k}") - print(" Note: the arch parameter must be specified if the model is not llama") - sys.exit(1) - - if suffix == ".lora_A.weight": - tname += ".weight.loraA" - elif suffix == ".lora_B.weight": - tname += ".weight.loraB" - else: - assert False - - print(f"{k} => {tname} {t.shape} {t.dtype} {t.nbytes/1024/1024:.2f}MB") - write_tensor_header(fout, tname, t.shape, t.dtype) - t.tofile(fout) - -print(f"Converted {input_json} and {input_model} to {output_path}") + + t = v.detach().numpy() + + prefix = "base_model.model." + if k.startswith(prefix): + k = k[len(prefix) :] + + lora_suffixes = (".lora_A.weight", ".lora_B.weight") + if k.endswith(lora_suffixes): + suffix = k[-len(lora_suffixes[0]):] + k = k[: -len(lora_suffixes[0])] + else: + print(f"Error: unrecognized tensor name {orig_k}") + sys.exit(1) + + tname = name_map.get_name(k) + if tname is None: + print(f"Error: could not map tensor name {orig_k}") + print(" Note: the arch parameter must be specified if the model is not llama") + sys.exit(1) + + if suffix == ".lora_A.weight": + tname += ".weight.loraA" + elif suffix == ".lora_B.weight": + tname += ".weight.loraB" + else: + assert False + + print(f"{k} => {tname} {t.shape} {t.dtype} {t.nbytes/1024/1024:.2f}MB") + write_tensor_header(fout, tname, t.shape, t.dtype) + t.tofile(fout) + + print(f"Converted {input_json} and {input_model} to {output_path}") From d0ab7a1dc66f864a868769974bae1cafacfa2538 Mon Sep 17 00:00:00 2001 From: Jared Van Bortel Date: Wed, 27 Dec 2023 21:34:46 -0500 Subject: [PATCH 08/11] small syntax change --- check-requirements.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check-requirements.sh b/check-requirements.sh index 77ed98fcb1f22..4f7cfcf69a41c 100755 --- a/check-requirements.sh +++ b/check-requirements.sh @@ -113,7 +113,7 @@ check_convert_script() { check_requirements "$reqs" - python - "$py" "$pyname" < Date: Wed, 27 Dec 2023 21:38:54 -0500 Subject: [PATCH 09/11] don't remove venvs if nocleanup is passed --- check-requirements.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/check-requirements.sh b/check-requirements.sh index 4f7cfcf69a41c..c6341cbbca03d 100755 --- a/check-requirements.sh +++ b/check-requirements.sh @@ -58,9 +58,12 @@ cleanup() { fi } +do_cleanup=1 if [[ ${1-} == nocleanup ]]; then - shift # discard nocleanup arg -else + do_cleanup=0; shift +fi + +if (( do_cleanup )); then trap exit INT TERM trap cleanup EXIT fi @@ -121,7 +124,9 @@ SourceFileLoader(pyname, py).load_module() EOF ) - rm -rf -- "$venv" + if (( do_cleanup )); then + rm -rf -- "$venv" + fi info "$py: imports OK" } @@ -156,7 +161,9 @@ python3 -m venv "$all_venv" check_requirements requirements.txt ) -rm -rf -- "$all_venv" +if (( do_cleanup )); then + rm -rf -- "$all_venv" +fi check_convert_script convert.py for py in convert-*.py; do From b6bf2643a77c28921ff05542ca8e40e0e63e2df8 Mon Sep 17 00:00:00 2001 From: crasm Date: Thu, 28 Dec 2023 04:20:24 -0500 Subject: [PATCH 10/11] See if this fixes docker workflow --- .devops/full-cuda.Dockerfile | 3 ++- .devops/full-rocm.Dockerfile | 3 ++- .devops/full.Dockerfile | 3 ++- .devops/main-rocm.Dockerfile | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.devops/full-cuda.Dockerfile b/.devops/full-cuda.Dockerfile index 360602d6567b8..77a9ddc145d0b 100644 --- a/.devops/full-cuda.Dockerfile +++ b/.devops/full-cuda.Dockerfile @@ -14,7 +14,8 @@ ARG CUDA_DOCKER_ARCH=all RUN apt-get update && \ apt-get install -y build-essential python3 python3-pip git -COPY requirements.txt requirements.txt +COPY requirements.txt requirements.txt +COPY requirements requirements RUN pip install --upgrade pip setuptools wheel \ && pip install -r requirements.txt diff --git a/.devops/full-rocm.Dockerfile b/.devops/full-rocm.Dockerfile index 6c521e9b4101f..8b9633dc4ebf5 100644 --- a/.devops/full-rocm.Dockerfile +++ b/.devops/full-rocm.Dockerfile @@ -23,7 +23,8 @@ ARG ROCM_DOCKER_ARCH=\ gfx1101 \ gfx1102 -COPY requirements.txt requirements.txt +COPY requirements.txt requirements.txt +COPY requirements requirements RUN pip install --upgrade pip setuptools wheel \ && pip install -r requirements.txt diff --git a/.devops/full.Dockerfile b/.devops/full.Dockerfile index 687628b35e996..cef1297d3e156 100644 --- a/.devops/full.Dockerfile +++ b/.devops/full.Dockerfile @@ -5,7 +5,8 @@ FROM ubuntu:$UBUNTU_VERSION as build RUN apt-get update && \ apt-get install -y build-essential python3 python3-pip git -COPY requirements.txt requirements.txt +COPY requirements.txt requirements.txt +COPY requirements requirements RUN pip install --upgrade pip setuptools wheel \ && pip install -r requirements.txt diff --git a/.devops/main-rocm.Dockerfile b/.devops/main-rocm.Dockerfile index 789deff6dc8c1..0a706dc73227d 100644 --- a/.devops/main-rocm.Dockerfile +++ b/.devops/main-rocm.Dockerfile @@ -23,7 +23,8 @@ ARG ROCM_DOCKER_ARCH=\ gfx1101 \ gfx1102 -COPY requirements.txt requirements.txt +COPY requirements.txt requirements.txt +COPY requirements requirements RUN pip install --upgrade pip setuptools wheel \ && pip install -r requirements.txt From 5eb01d4e64f3331cae854a3d275cb7da202b6371 Mon Sep 17 00:00:00 2001 From: crasm Date: Thu, 28 Dec 2023 04:41:59 -0500 Subject: [PATCH 11/11] Move check-requirements.sh into ./scripts/ --- .github/workflows/python-check-requirements.yml | 6 +++--- check-requirements.sh => scripts/check-requirements.sh | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) rename check-requirements.sh => scripts/check-requirements.sh (95%) diff --git a/.github/workflows/python-check-requirements.yml b/.github/workflows/python-check-requirements.yml index 6a27c618ff9c1..92e1108b3af88 100644 --- a/.github/workflows/python-check-requirements.yml +++ b/.github/workflows/python-check-requirements.yml @@ -3,13 +3,13 @@ name: Python check requirements.txt on: push: paths: - - 'check-requirements.sh' + - 'scripts/check-requirements.sh' - 'convert*.py' - 'requirements.txt' - 'requirements/*.txt' pull_request: paths: - - 'check-requirements.sh' + - 'scripts/check-requirements.sh' - 'convert*.py' - 'requirements.txt' - 'requirements/*.txt' @@ -26,4 +26,4 @@ jobs: with: python-version: "3.11" - name: Run check-requirements.sh script - run: bash check-requirements.sh nocleanup + run: bash scripts/check-requirements.sh nocleanup diff --git a/check-requirements.sh b/scripts/check-requirements.sh similarity index 95% rename from check-requirements.sh rename to scripts/check-requirements.sh index c6341cbbca03d..af7bab7533789 100755 --- a/check-requirements.sh +++ b/scripts/check-requirements.sh @@ -1,5 +1,6 @@ #!/bin/bash set -euo pipefail + # # check-requirements.sh checks all requirements files for each top-level # convert*.py script. @@ -8,8 +9,8 @@ set -euo pipefail # python script. As of 2023-12-22, this writes ~2.7GB of data. An adequately # sized tmpfs /tmp or ramdisk is recommended if running this frequently. # -# usage: ./check-requirements.sh [] -# ./check-requirements.sh nocleanup [] +# usage: check-requirements.sh [] +# check-requirements.sh nocleanup [] # # where: # - is a directory that can be used as the base for @@ -69,7 +70,7 @@ if (( do_cleanup )); then fi this=$(realpath -- "$0"); readonly this -cd "$(dirname "$this")" +cd "$(dirname "$this")/.." # PWD should stay in llama.cpp project directory shellcheck "$this"