mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-11-01 08:27:39 -06:00
Compare commits
373
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58240ea438 | ||
|
|
ebe99198c9 | ||
|
|
ce4a160787 | ||
|
|
889f2ec96d | ||
|
|
1ab23c33b7 | ||
|
|
27b8464e82 | ||
|
|
d63fbfe794 | ||
|
|
e216f4ec87 | ||
|
|
344846b4fc | ||
|
|
fb5997274e | ||
|
|
ab3768cdc2 | ||
|
|
e0f142d5dd | ||
|
|
3c809f4438 | ||
|
|
36a0e2ffc0 | ||
|
|
418f905a4e | ||
|
|
eb4e244be7 | ||
|
|
68d20b884c | ||
|
|
0893ec98f5 | ||
|
|
78feec062d | ||
|
|
0ac1a7fd35 | ||
|
|
7df8b876b9 | ||
|
|
8ab29faec4 | ||
|
|
f5d4d75419 | ||
|
|
3ed915e309 | ||
|
|
f8fcef2546 | ||
|
|
b7cde589e4 | ||
|
|
3dd0f60bb4 | ||
|
|
78febbad9d | ||
|
|
abb76ab380 | ||
|
|
41be0bdc98 | ||
|
|
22183e51f9 | ||
|
|
6d194531e1 | ||
|
|
fade717ed8 | ||
|
|
c9daee149d | ||
|
|
8a199ad31a | ||
|
|
da6c30ade8 | ||
|
|
f7c01c5226 | ||
|
|
bf48103ad1 | ||
|
|
6d5ac3c8d5 | ||
|
|
7ac5727d60 | ||
|
|
2e0d86bebc | ||
|
|
e09a78a25d | ||
|
|
369dcf309c | ||
|
|
d0ff6317cc | ||
|
|
cf24317bea | ||
|
|
74a4fb0037 | ||
|
|
5220213255 | ||
|
|
c3df4b7e22 | ||
|
|
ac8e1694d8 | ||
|
|
c02b5c641e | ||
|
|
ff0d396e3d | ||
|
|
838093f192 | ||
|
|
604cc43023 | ||
|
|
39febd2adb | ||
|
|
f5222f25c1 | ||
|
|
28cb47c7c8 | ||
|
|
8642541b25 | ||
|
|
d4f1b18b3a | ||
|
|
d1eb0debad | ||
|
|
5d7793d6fc | ||
|
|
ec941ee852 | ||
|
|
e486a4b559 | ||
|
|
f1a7dea99a | ||
|
|
a7af149a2a | ||
|
|
2864937a77 | ||
|
|
a71c00f0bb | ||
|
|
f4d8af608a | ||
|
|
02d502f314 | ||
|
|
b1756b23f3 | ||
|
|
6886628cb1 | ||
|
|
8ca4060b7e | ||
|
|
d05706b4f3 | ||
|
|
1b4554eff1 | ||
|
|
a919179942 | ||
|
|
338fce6511 | ||
|
|
19ff817fb2 | ||
|
|
eb7818c0b8 | ||
|
|
6c44591049 | ||
|
|
19cce69199 | ||
|
|
e86f5fd1e9 | ||
|
|
27b7a8a009 | ||
|
|
7ecb57f7ad | ||
|
|
8542e94350 | ||
|
|
922f53ed33 | ||
|
|
0c8c59ef3d | ||
|
|
3ab37aedfe | ||
|
|
85cc432e2c | ||
|
|
74a1fd3d03 | ||
|
|
7eed6b7685 | ||
|
|
276378515c | ||
|
|
d78f40d9d3 | ||
|
|
fae9ac561f | ||
|
|
a8276b70cc | ||
|
|
de5122d879 | ||
|
|
6666a7330c | ||
|
|
7a2e5aba90 | ||
|
|
0d5ca60098 | ||
|
|
d930fe3c26 | ||
|
|
a2b3e121ce | ||
|
|
823df7d0fb | ||
|
|
1a97e387d3 | ||
|
|
bc262f33cc | ||
|
|
f5892ca5ad | ||
|
|
7aca0d2ea8 | ||
|
|
961c376a2d | ||
|
|
4244ad7acd | ||
|
|
63b4b303bd | ||
|
|
caa8191df1 | ||
|
|
a4699400e3 | ||
|
|
4b851fbee8 | ||
|
|
4d71b42fd8 | ||
|
|
70c5f26324 | ||
|
|
5cd62ffee9 | ||
|
|
38645c2ad9 | ||
|
|
02269b4c00 | ||
|
|
5320c87ff0 | ||
|
|
74b104c9ba | ||
|
|
c6fb3b5be2 | ||
|
|
8b28c23943 | ||
|
|
fd8f8451a2 | ||
|
|
9125a2ebe8 | ||
|
|
9b8446b914 | ||
|
|
dc9e3bed42 | ||
|
|
69b3cc406e | ||
|
|
224d94c02b | ||
|
|
48c36110ab | ||
|
|
a074215352 | ||
|
|
d854d98bae | ||
|
|
48c9f08b7b | ||
|
|
6050b467c0 | ||
|
|
2280f0fd16 | ||
|
|
1a0b56cec7 | ||
|
|
63412d8737 | ||
|
|
5c965fd8ca | ||
|
|
f1168ac417 | ||
|
|
fa3645feb5 | ||
|
|
7790a9231d | ||
|
|
c49edf0963 | ||
|
|
a55c16e4ee | ||
|
|
e321bdad7e | ||
|
|
459a1ba549 | ||
|
|
f0fb5984b0 | ||
|
|
9a4580b61c | ||
|
|
b083c384f7 | ||
|
|
344be0a600 | ||
|
|
b3fa69955e | ||
|
|
74bca39500 | ||
|
|
d327bf1f07 | ||
|
|
78a8ae14be | ||
|
|
f3269ef96a | ||
|
|
3f03b17724 | ||
|
|
9a0fde9967 | ||
|
|
e168e041ec | ||
|
|
ea1d7d66f3 | ||
|
|
71255fefc0 | ||
|
|
73adf89e38 | ||
|
|
df9beac715 | ||
|
|
4d32b29aaa | ||
|
|
3032209758 | ||
|
|
701fd25b3a | ||
|
|
3642238cc1 | ||
|
|
6b9ba4f2e8 | ||
|
|
8e5a58df87 | ||
|
|
1ddffe9f70 | ||
|
|
7894e8b924 | ||
|
|
edc353842c | ||
|
|
9ae73e68f5 | ||
|
|
0ae0e19766 | ||
|
|
21ec307146 | ||
|
|
9145ddb955 | ||
|
|
1a24df1639 | ||
|
|
9ff0d7e90f | ||
|
|
06230cb877 | ||
|
|
5c31142682 | ||
|
|
da2ee1e7a6 | ||
|
|
280141e6d4 | ||
|
|
36e36e63a5 | ||
|
|
e1e3b2c409 | ||
|
|
4231b23dc2 | ||
|
|
f00c9083b2 | ||
|
|
f5027fb213 | ||
|
|
f64a49819c | ||
|
|
1ab09d632e | ||
|
|
f9599d0cc8 | ||
|
|
643b0cdacb | ||
|
|
0a9ef00678 | ||
|
|
7377bada8c | ||
|
|
184f653602 | ||
|
|
d814cbb88c | ||
|
|
970b51ce7c | ||
|
|
a08ab75ae7 | ||
|
|
19ab5a5e65 | ||
|
|
36cbf415f1 | ||
|
|
5c19a5a756 | ||
|
|
d9ee007a06 | ||
|
|
d81d0d8fb9 | ||
|
|
d77256999f | ||
|
|
0693dbfb91 | ||
|
|
a5d22276be | ||
|
|
5f5acfe250 | ||
|
|
1f4c2eeaa5 | ||
|
|
7170cad4d0 | ||
|
|
d88040c704 | ||
|
|
e197cef455 | ||
|
|
bf039c8496 | ||
|
|
dfeea52005 | ||
|
|
e3797b184d | ||
|
|
0b1281aeef | ||
|
|
4912ac24f1 | ||
|
|
84746c5c94 | ||
|
|
0682a576a7 | ||
|
|
a6b83d426f | ||
|
|
c7fc509417 | ||
|
|
a37cc46104 | ||
|
|
99fb997184 | ||
|
|
7775ffb7e4 | ||
|
|
d454a204ef | ||
|
|
0756e83fc1 | ||
|
|
9febe63d5a | ||
|
|
9b56ccf00d | ||
|
|
0a1ef309cd | ||
|
|
261700b896 | ||
|
|
f849095365 | ||
|
|
824eaecf48 | ||
|
|
eb182d85c0 | ||
|
|
ea9e3446b5 | ||
|
|
0efe8e4c6b | ||
|
|
b531e4c749 | ||
|
|
74b115ca6e | ||
|
|
96caa80059 | ||
|
|
479a52779f | ||
|
|
8baabd53b7 | ||
|
|
493253fe5a | ||
|
|
339f3242c7 | ||
|
|
9a3b91c680 | ||
|
|
5c0b49a23b | ||
|
|
1242be66f6 | ||
|
|
84e66d0321 | ||
|
|
52ffa02745 | ||
|
|
839a572a4c | ||
|
|
ead9f32058 | ||
|
|
b684b498b6 | ||
|
|
4a45de4f2c | ||
|
|
7e3eeee048 | ||
|
|
5ba3b9fd3d | ||
|
|
5a9a34d888 | ||
|
|
39da130fcd | ||
|
|
4e201388c3 | ||
|
|
1ba568c5ad | ||
|
|
36140dbee2 | ||
|
|
df11702136 | ||
|
|
9df0b73c70 | ||
|
|
b38960f520 | ||
|
|
a01ce9ffc6 | ||
|
|
8115b4c31f | ||
|
|
119f68d7d4 | ||
|
|
b684498579 | ||
|
|
522f2b6eed | ||
|
|
83c2d012a2 | ||
|
|
13786fbb3a | ||
|
|
d80619397c | ||
|
|
704366d76f | ||
|
|
707616feb5 | ||
|
|
d8d92973c8 | ||
|
|
7879491cf4 | ||
|
|
8466395353 | ||
|
|
dd002b91f5 | ||
|
|
1d88cb05f1 | ||
|
|
3bb63cbcbf | ||
|
|
f711c8f303 | ||
|
|
921257f500 | ||
|
|
3ffb62935b | ||
|
|
7adf160c41 | ||
|
|
ab7ed0d4b5 | ||
|
|
4c3c5810f8 | ||
|
|
1b29621bac | ||
|
|
6fa4f6cd3e | ||
|
|
0ce8cd1141 | ||
|
|
f174553e16 | ||
|
|
7951f601e8 | ||
|
|
33d6135cbe | ||
|
|
098ce526b5 | ||
|
|
f3fb2fcdbc | ||
|
|
fda00f18cd | ||
|
|
4866db350d | ||
|
|
42b70032b1 | ||
|
|
4c1004c492 | ||
|
|
412a363c65 | ||
|
|
2b57148b08 | ||
|
|
8f97d82da3 | ||
|
|
27af58168a | ||
|
|
7db0d3d781 | ||
|
|
99db719bdf | ||
|
|
31288c9c66 | ||
|
|
fe8841a78a | ||
|
|
ff5a233767 | ||
|
|
0878f7c304 | ||
|
|
5aeac37a35 | ||
|
|
547208dca2 | ||
|
|
848bbda904 | ||
|
|
797de63b2f | ||
|
|
9d1878a435 | ||
|
|
c0fd786264 | ||
|
|
2744e48d1e | ||
|
|
10b8b75534 | ||
|
|
9d1e03616e | ||
|
|
f2f667b02d | ||
|
|
62318cd57d | ||
|
|
83f9389419 | ||
|
|
1268edac57 | ||
|
|
f74e2fa6a3 | ||
|
|
ea0c4c2c85 | ||
|
|
6420cf5dd5 | ||
|
|
27c7a769da | ||
|
|
2f2763db6e | ||
|
|
01fc3aae84 | ||
|
|
23f7aabc16 | ||
|
|
7182ef71e6 | ||
|
|
2a667032dc | ||
|
|
f3363593f4 | ||
|
|
ceefd16daa | ||
|
|
9a16f1dbe4 | ||
|
|
b84ea5cf5e | ||
|
|
a30fb4924d | ||
|
|
495c5805ce | ||
|
|
623c16393c | ||
|
|
13b8048920 | ||
|
|
0f4376268f | ||
|
|
3dbcb7fca5 | ||
|
|
c804754529 | ||
|
|
c0083e3052 | ||
|
|
13de11e78f | ||
|
|
88aaf325a7 | ||
|
|
a8d9a7e1e5 | ||
|
|
f12aaa344c | ||
|
|
5e03a1ff45 | ||
|
|
b797b9ca81 | ||
|
|
6afa9a3134 | ||
|
|
b6334e37e6 | ||
|
|
e707790c73 | ||
|
|
43d57fb8ca | ||
|
|
2851b3bcbf | ||
|
|
59d5fad7c1 | ||
|
|
e287bd8729 | ||
|
|
2f729cfa4c | ||
|
|
ba3c05f71d | ||
|
|
b76b13379d | ||
|
|
620d2225cc | ||
|
|
cd0a5b6a18 | ||
|
|
a11b6530b5 | ||
|
|
61ad80bf93 | ||
|
|
a6b91d8c5e | ||
|
|
ea736c1d5c | ||
|
|
b0d239164b | ||
|
|
c0cb53ac4e | ||
|
|
4ed1c8507e | ||
|
|
711780ffc7 | ||
|
|
fd55cb98fe | ||
|
|
3e4dfa8f39 | ||
|
|
08698c81ed | ||
|
|
82e1e2fd84 | ||
|
|
47f5a64ae1 | ||
|
|
e36df60470 | ||
|
|
9166ceaf16 | ||
|
|
2130ca8476 | ||
|
|
23f83e056d | ||
|
|
2ce20d22fd | ||
|
|
68ac765726 | ||
|
|
77c1f39929 | ||
|
|
733f57a999 | ||
|
|
5a638aedb7 | ||
|
|
6e66cb5d4e | ||
|
|
e467e5b5e5 |
@@ -19,7 +19,12 @@ RUN npm ci
|
||||
COPY tools/ui/ ./
|
||||
RUN LLAMA_BUILD_NUMBER="$APP_VERSION" npm run build
|
||||
|
||||
FROM docker.io/intel/oneapi-toolkit:$ONEAPI_VERSION AS build
|
||||
FROM docker.io/intel/deep-learning-essentials:$ONEAPI_VERSION AS build
|
||||
|
||||
RUN echo "Download oneDNN installation package" && \
|
||||
wget https://registrationcenter-download.intel.com/akdlm/IRC_NAS/94c3dbac-0852-45be-a57d-21c204cada3e/intel-onednn-2026.0.2.46_offline.sh -O intel-onednn_offline.sh && \
|
||||
echo "Install oneDNN components" && \
|
||||
bash intel-onednn_offline.sh -s -a --silent --eula accept
|
||||
|
||||
ARG GGML_SYCL_F16=ON
|
||||
ARG LEVEL_ZERO_VERSION=1.28.2
|
||||
@@ -45,7 +50,7 @@ RUN if [ "${GGML_SYCL_F16}" = "ON" ]; then \
|
||||
fi && \
|
||||
echo "Building with dynamic libs" && \
|
||||
cmake -B build -DGGML_NATIVE=OFF -DGGML_SYCL=ON -DCMAKE_C_COMPILER=icx -DCMAKE_CXX_COMPILER=icpx -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON -DLLAMA_BUILD_TESTS=OFF ${OPT_SYCL_F16} && \
|
||||
cmake --build build --config Release -j$(nproc)
|
||||
cmake --build build --config Release -j$((($(nproc)+1)/2))
|
||||
|
||||
RUN mkdir -p /app/lib && \
|
||||
find build -name "*.so*" -exec cp -P {} /app/lib \;
|
||||
@@ -102,7 +107,7 @@ RUN mkdir /tmp/neo/ && cd /tmp/neo/ \
|
||||
&& dpkg --install *.deb
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libgomp1 curl ffmpeg \
|
||||
&& apt-get install -y libgomp1 curl ffmpeg openssl libssl-dev \
|
||||
&& apt autoremove -y \
|
||||
&& apt clean -y \
|
||||
&& rm -rf /tmp/* /var/tmp/* \
|
||||
|
||||
@@ -5,6 +5,8 @@ exclude =
|
||||
# Do not traverse examples and tools
|
||||
examples,
|
||||
tools,
|
||||
# Do not traverse .venv directories
|
||||
.venv*,
|
||||
# Do not include package initializers
|
||||
__init__.py,
|
||||
# No need to traverse our git directory
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
name: Behavior Check
|
||||
|
||||
on:
|
||||
workflow_dispatch: # allows manual triggering
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths: [
|
||||
'.github/workflows/behavior-check.yml',
|
||||
'**/CMakeLists.txt',
|
||||
'**/.cmake',
|
||||
'**/*.h',
|
||||
'**/*.hpp',
|
||||
'**/*.c',
|
||||
'**/*.cpp',
|
||||
'**/*.py',
|
||||
'**/requirements*.txt',
|
||||
]
|
||||
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths: [
|
||||
'.github/workflows/behavior-check.yml',
|
||||
'**/CMakeLists.txt',
|
||||
'**/.cmake',
|
||||
'**/*.h',
|
||||
'**/*.hpp',
|
||||
'**/*.c',
|
||||
'**/*.cpp',
|
||||
'**/*.py',
|
||||
'**/requirements*.txt',
|
||||
]
|
||||
|
||||
cache-mode: none
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GGML_NLOOP: 3
|
||||
GGML_N_THREADS: 1
|
||||
GGML_SCHED_DEBUG_REALLOC: 1
|
||||
LLAMA_ARG_LOG_COLORS: 1
|
||||
LLAMA_ARG_LOG_PREFIX: 1
|
||||
LLAMA_ARG_LOG_TIMESTAMPS: 1
|
||||
|
||||
jobs:
|
||||
ubuntu:
|
||||
runs-on: ubuntu-26.04-arm
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
with:
|
||||
restore: false
|
||||
save: false
|
||||
|
||||
- name: Build Dependencies
|
||||
id: build_depends
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y build-essential libssl-dev git-lfs libjpeg-dev python3 python3-venv python3-pip
|
||||
|
||||
- name: Python Dependencies
|
||||
id: python_depends
|
||||
run: |
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements/requirements-convert_hf_to_gguf.txt
|
||||
|
||||
- name: ccache-buckets-restore
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CI }}
|
||||
with:
|
||||
key: behavior-check-ubuntu-26.04-arm
|
||||
folder: llama.cpp
|
||||
hf_bucket: ggml-org/cache
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
cmake -B build \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DGGML_BACKEND_DL=OFF \
|
||||
-DLLAMA_FATAL_WARNINGS=ON
|
||||
time cmake --build build --config Release -j $(nproc)
|
||||
|
||||
- name: ccache-buckets-save
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CACHE_OUTPUT }}
|
||||
with:
|
||||
key: behavior-check-ubuntu-26.04-arm
|
||||
folder: llama.cpp
|
||||
evict-old-files: 1d
|
||||
hf_bucket: ggml-org/cache
|
||||
save: true
|
||||
|
||||
- name: Test
|
||||
id: cmake_test
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
cd build
|
||||
ctest -L behavior -LE 'model|backend' --verbose --timeout 900
|
||||
|
||||
windows:
|
||||
runs-on: windows-2025
|
||||
|
||||
cache-mode: write
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
with:
|
||||
key: behavior-check-windows-2025
|
||||
evict-old-files: 1d
|
||||
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
|
||||
- name: Python Dependencies
|
||||
id: python_depends
|
||||
shell: bash
|
||||
run: |
|
||||
python3 -m venv .venv
|
||||
source .venv/Scripts/activate
|
||||
pip install -r requirements/requirements-convert_hf_to_gguf.txt
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
shell: cmd
|
||||
run: |
|
||||
call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64
|
||||
cmake -B build -G "Ninja Multi-Config" ^
|
||||
-DCMAKE_TOOLCHAIN_FILE=cmake/x64-windows-llvm.cmake ^
|
||||
-DCMAKE_BUILD_TYPE=Release ^
|
||||
-DGGML_NATIVE=OFF ^
|
||||
-DGGML_BACKEND_DL=OFF
|
||||
set /A NINJA_JOBS=%NUMBER_OF_PROCESSORS%-1
|
||||
cmake --build build --config Release -j %NINJA_JOBS%
|
||||
|
||||
- name: Test
|
||||
id: cmake_test
|
||||
shell: bash
|
||||
run: |
|
||||
source .venv/Scripts/activate
|
||||
cd build
|
||||
ctest -C Release -L behavior -LE 'model|backend' --verbose --timeout 900
|
||||
|
||||
- name: ccache-clear
|
||||
uses: ./.github/actions/ccache-clear
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
key: behavior-check-windows-2025
|
||||
older: 5m
|
||||
min: 1
|
||||
dry-run: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }}
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
android-ndk-snapdragon:
|
||||
runs-on: ubuntu-24.04 # previously ubuntu-latest
|
||||
container:
|
||||
image: 'ghcr.io/snapdragon-toolchain/arm64-android:v0.7'
|
||||
image: 'ghcr.io/snapdragon-toolchain/arm64-android:v0.8'
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
linux-iot-snapdragon:
|
||||
runs-on: ubuntu-24.04 # previously ubuntu-latest
|
||||
container:
|
||||
image: 'ghcr.io/snapdragon-toolchain/arm64-linux:v0.7'
|
||||
image: 'ghcr.io/snapdragon-toolchain/arm64-linux:v0.8'
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -104,11 +104,11 @@ jobs:
|
||||
name: Test on QDC Device (${{ matrix.device }})
|
||||
needs: [android-ndk-snapdragon, linux-iot-snapdragon]
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 90
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
device: [SM8750, SM8850, QCS9075M]
|
||||
device: [SM8850, QCS9075M]
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
--pkg-dir pkg-snapdragon/llama.cpp \
|
||||
--model-url "https://huggingface.co/bartowski/Llama-3.2-1B-Instruct-GGUF/resolve/main/Llama-3.2-1B-Instruct-Q4_0.gguf" \
|
||||
--device ${{ matrix.device }} \
|
||||
${{ startsWith(matrix.device, 'QCS') && '--retries 2 --retry-delay 300' || '' }}
|
||||
${{ startsWith(matrix.device, 'QCS') && '--job-timeout 5400 --retries 2 --retry-delay 300' || '' }}
|
||||
env:
|
||||
QDC_API_KEY: ${{ secrets.QDC_API_KEY }}
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
name: CI (apple - daily)
|
||||
|
||||
on:
|
||||
workflow_dispatch: # allows manual triggering
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
cache-mode: none
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GGML_NLOOP: 3
|
||||
GGML_N_THREADS: 1
|
||||
LLAMA_ARG_LOG_COLORS: 1
|
||||
LLAMA_ARG_LOG_PREFIX: 1
|
||||
LLAMA_ARG_LOG_TIMESTAMPS: 1
|
||||
|
||||
jobs:
|
||||
macos-latest-ios-xcode:
|
||||
runs-on: macos-latest
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: ggml-org/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: latest-stable
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
-DLLAMA_BUILD_EXAMPLES=OFF \
|
||||
-DLLAMA_BUILD_TOOLS=OFF \
|
||||
-DLLAMA_BUILD_TESTS=OFF \
|
||||
-DLLAMA_BUILD_SERVER=OFF \
|
||||
-DCMAKE_SYSTEM_NAME=iOS \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 \
|
||||
-DCMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM=ggml
|
||||
cmake --build build --config Release -j $(sysctl -n hw.logicalcpu) -- CODE_SIGNING_ALLOWED=NO
|
||||
|
||||
- name: xcodebuild for swift package
|
||||
id: xcodebuild
|
||||
run: |
|
||||
./build-xcframework.sh
|
||||
|
||||
- name: Upload xcframework artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: llama-xcframework
|
||||
path: build-apple/llama.xcframework/
|
||||
retention-days: 1
|
||||
|
||||
- name: Build Xcode project
|
||||
run: |
|
||||
xcodebuild -downloadPlatform iOS
|
||||
xcodebuild -project examples/llama.swiftui/llama.swiftui.xcodeproj -scheme llama.swiftui -sdk iphoneos CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= -destination 'generic/platform=iOS' FRAMEWORK_FOLDER_PATH=./build-ios build
|
||||
|
||||
macos-latest-tvos:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_BUILD_COMMON=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
-DLLAMA_BUILD_EXAMPLES=OFF \
|
||||
-DLLAMA_BUILD_TOOLS=OFF \
|
||||
-DLLAMA_BUILD_TESTS=OFF \
|
||||
-DLLAMA_BUILD_SERVER=OFF \
|
||||
-DCMAKE_SYSTEM_NAME=tvOS \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 \
|
||||
-DCMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM=ggml
|
||||
cmake --build build --config Release -j $(sysctl -n hw.logicalcpu) -- CODE_SIGNING_ALLOWED=NO
|
||||
|
||||
macos-latest-visionos:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_BUILD_COMMON=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
-DLLAMA_BUILD_EXAMPLES=OFF \
|
||||
-DLLAMA_BUILD_TOOLS=OFF \
|
||||
-DLLAMA_BUILD_TESTS=OFF \
|
||||
-DLLAMA_BUILD_SERVER=OFF \
|
||||
-DCMAKE_SYSTEM_NAME=visionOS \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=1.0 \
|
||||
-DCMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM=ggml
|
||||
cmake --build build --config Release -j $(sysctl -n hw.logicalcpu) -- CODE_SIGNING_ALLOWED=NO
|
||||
|
||||
macos-latest-swift:
|
||||
runs-on: macos-latest
|
||||
needs: macos-latest-ios-xcode
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
destination: ['generic/platform=macOS', 'generic/platform=iOS', 'generic/platform=tvOS']
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download xcframework artifact
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: llama-xcframework
|
||||
path: build-apple/llama.xcframework/
|
||||
|
||||
- name: Build llama.cpp with CMake
|
||||
id: cmake_build
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
-DLLAMA_BUILD_EXAMPLES=OFF \
|
||||
-DLLAMA_BUILD_TOOLS=OFF \
|
||||
-DLLAMA_BUILD_TESTS=OFF \
|
||||
-DLLAMA_BUILD_SERVER=OFF \
|
||||
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
|
||||
cmake --build build --config Release -j $(sysctl -n hw.logicalcpu)
|
||||
@@ -105,198 +105,5 @@ jobs:
|
||||
cd build
|
||||
# Metal Paravirtual devices are difficult to support -> disable
|
||||
# ref: https://github.com/ggml-org/llama.cpp/pull/19802#issuecomment-4013704023
|
||||
ctest -L main -E "test-llama-archs|test-llama-context" --verbose --timeout 900
|
||||
ctest -L main -E "test-llama-archs|test-llama-context|test-speculative-rejection-sampling" --verbose --timeout 900
|
||||
|
||||
macos-latest-x64:
|
||||
runs-on: macos-15-intel
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
with:
|
||||
restore: false
|
||||
save: false
|
||||
|
||||
- name: ccache-buckets-restore
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CI }}
|
||||
with:
|
||||
key: apple-x64
|
||||
folder: llama.cpp
|
||||
hf_bucket: ggml-org/cache
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
sysctl -a
|
||||
# Metal is disabled due to intermittent failures with Github runners not having a GPU:
|
||||
# https://github.com/ggml-org/llama.cpp/actions/runs/8635935781/job/23674807267#step:5:2313
|
||||
cmake -B build \
|
||||
-DCMAKE_BUILD_RPATH="@loader_path" \
|
||||
-DLLAMA_FATAL_WARNINGS=ON \
|
||||
-DLLAMA_BUILD_BORINGSSL=ON \
|
||||
-DGGML_METAL=OFF \
|
||||
-DGGML_RPC=ON \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=13.3
|
||||
time cmake --build build --config Release -j $(sysctl -n hw.logicalcpu)
|
||||
|
||||
- name: ccache-buckets-save
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CACHE_OUTPUT }}
|
||||
with:
|
||||
key: apple-x64
|
||||
folder: llama.cpp
|
||||
evict-old-files: 1d
|
||||
hf_bucket: ggml-org/cache
|
||||
save: true
|
||||
|
||||
- name: Test
|
||||
id: cmake_test
|
||||
run: |
|
||||
cd build
|
||||
ctest -L main --verbose --timeout 900
|
||||
|
||||
macos-latest-ios-xcode:
|
||||
runs-on: macos-latest
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: ggml-org/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: latest-stable
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
-DLLAMA_BUILD_EXAMPLES=OFF \
|
||||
-DLLAMA_BUILD_TOOLS=OFF \
|
||||
-DLLAMA_BUILD_TESTS=OFF \
|
||||
-DLLAMA_BUILD_SERVER=OFF \
|
||||
-DCMAKE_SYSTEM_NAME=iOS \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 \
|
||||
-DCMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM=ggml
|
||||
cmake --build build --config Release -j $(sysctl -n hw.logicalcpu) -- CODE_SIGNING_ALLOWED=NO
|
||||
|
||||
- name: xcodebuild for swift package
|
||||
id: xcodebuild
|
||||
run: |
|
||||
./build-xcframework.sh
|
||||
|
||||
- name: Upload xcframework artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: llama-xcframework
|
||||
path: build-apple/llama.xcframework/
|
||||
retention-days: 1
|
||||
|
||||
- name: Build Xcode project
|
||||
run: |
|
||||
xcodebuild -downloadPlatform iOS
|
||||
xcodebuild -project examples/llama.swiftui/llama.swiftui.xcodeproj -scheme llama.swiftui -sdk iphoneos CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= -destination 'generic/platform=iOS' FRAMEWORK_FOLDER_PATH=./build-ios build
|
||||
|
||||
macos-latest-tvos:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_BUILD_COMMON=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
-DLLAMA_BUILD_EXAMPLES=OFF \
|
||||
-DLLAMA_BUILD_TOOLS=OFF \
|
||||
-DLLAMA_BUILD_TESTS=OFF \
|
||||
-DLLAMA_BUILD_SERVER=OFF \
|
||||
-DCMAKE_SYSTEM_NAME=tvOS \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 \
|
||||
-DCMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM=ggml
|
||||
cmake --build build --config Release -j $(sysctl -n hw.logicalcpu) -- CODE_SIGNING_ALLOWED=NO
|
||||
|
||||
macos-latest-visionos:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_BUILD_COMMON=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
-DLLAMA_BUILD_EXAMPLES=OFF \
|
||||
-DLLAMA_BUILD_TOOLS=OFF \
|
||||
-DLLAMA_BUILD_TESTS=OFF \
|
||||
-DLLAMA_BUILD_SERVER=OFF \
|
||||
-DCMAKE_SYSTEM_NAME=visionOS \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=1.0 \
|
||||
-DCMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM=ggml
|
||||
cmake --build build --config Release -j $(sysctl -n hw.logicalcpu) -- CODE_SIGNING_ALLOWED=NO
|
||||
|
||||
macos-latest-swift:
|
||||
runs-on: macos-latest
|
||||
needs: macos-latest-ios-xcode
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
destination: ['generic/platform=macOS', 'generic/platform=iOS', 'generic/platform=tvOS']
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download xcframework artifact
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: llama-xcframework
|
||||
path: build-apple/llama.xcframework/
|
||||
|
||||
- name: Build llama.cpp with CMake
|
||||
id: cmake_build
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
-DLLAMA_BUILD_EXAMPLES=OFF \
|
||||
-DLLAMA_BUILD_TOOLS=OFF \
|
||||
-DLLAMA_BUILD_TESTS=OFF \
|
||||
-DLLAMA_BUILD_SERVER=OFF \
|
||||
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
|
||||
cmake --build build --config Release -j $(sysctl -n hw.logicalcpu)
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: [self-hosted, Linux, CPU]
|
||||
runs-on: [self-hosted, Linux, fast]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
id: cmake_test
|
||||
run: |
|
||||
cd build
|
||||
ctest -L 'main|python' --verbose --timeout 900
|
||||
ctest -L main --verbose --timeout 900
|
||||
|
||||
- name: Test llama2c conversion
|
||||
id: llama2c_test
|
||||
|
||||
@@ -20,7 +20,11 @@ on:
|
||||
paths: [
|
||||
'.github/workflows/build-ibm.yml',
|
||||
'ggml/src/ggml-cpu/**',
|
||||
'ggml/src/ggml-zdnn/**'
|
||||
'ggml/src/ggml-zdnn/**',
|
||||
'ggml/src/ggml-quants.c',
|
||||
'src/llama-quant.cpp',
|
||||
'gguf-py/gguf/scripts/gguf_convert_endian.py',
|
||||
'tests/test-quantize-be.sh'
|
||||
]
|
||||
|
||||
cache-mode: none
|
||||
@@ -39,9 +43,9 @@ env:
|
||||
LLAMA_ARG_LOG_TIMESTAMPS: 1
|
||||
|
||||
jobs:
|
||||
ubuntu-24-s390x:
|
||||
name: ubuntu-24-s390x (VXE ${{ matrix.vxe }})
|
||||
runs-on: ubuntu-24.04-s390x
|
||||
ubuntu-26-s390x:
|
||||
name: ubuntu-26-s390x (VXE ${{ matrix.vxe }})
|
||||
runs-on: ubuntu-26.04-s390x
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -62,12 +66,6 @@ jobs:
|
||||
libjpeg-dev build-essential libssl-dev \
|
||||
git-lfs
|
||||
|
||||
- name: Toolchain workaround (GCC 14)
|
||||
run: |
|
||||
sudo apt-get install -y gcc-14 g++-14
|
||||
echo "CC=gcc-14" >> "$GITHUB_ENV"
|
||||
echo "CXX=g++-14" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Python Dependencies
|
||||
id: python_depends
|
||||
run: |
|
||||
@@ -105,32 +103,26 @@ jobs:
|
||||
wget https://huggingface.co/ggml-org/models/resolve/main/tinyllamas/stories260K-be.gguf
|
||||
./bin/llama-completion -m stories260K-be.gguf -p "One day, Lily met a Shoggoth" -n 500 -c 256
|
||||
|
||||
- name: Test quantize (s390x)
|
||||
id: quantize_test_s390x
|
||||
run: |
|
||||
./tests/test-quantize-be.sh ./build/bin/llama-quantize ./build/bin/llama-perplexity 2>&1
|
||||
|
||||
ubuntu-26-zdnn-s390x:
|
||||
name: ubuntu-26-zdnn-s390x
|
||||
runs-on: ubuntu-24.04-s390x
|
||||
container: ubuntu:26.04 # required to get GCC 15.1 and binutils 2.44
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
runs-on: ubuntu-26.04-s390x
|
||||
|
||||
steps:
|
||||
- name: Build Dependencies
|
||||
id: build_depends
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential cmake git ca-certificates \
|
||||
libssl-dev libzdnn-dev
|
||||
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Toolchain workaround (GCC 15)
|
||||
- name: Build Dependencies
|
||||
id: build_depends
|
||||
run: |
|
||||
apt-get install -y gcc-15 g++-15
|
||||
echo "CC=gcc-15" >> "$GITHUB_ENV"
|
||||
echo "CXX=g++-15" >> "$GITHUB_ENV"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
build-essential libssl-dev libzdnn-dev
|
||||
|
||||
- name: Build with zDNN Backend
|
||||
id: cmake_build
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
- sanitizer: THREAD
|
||||
machine: ubuntu-24.04
|
||||
- sanitizer: UNDEFINED
|
||||
machine: [self-hosted, X64, Linux]
|
||||
machine: [self-hosted, X64, Linux, CPU]
|
||||
|
||||
runs-on: ${{ matrix.machine }}
|
||||
|
||||
|
||||
@@ -67,8 +67,15 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cd /tmp
|
||||
wget https://registrationcenter-download.intel.com/akdlm/IRC_NAS/5996e26b-f48a-42b1-8db0-b002ad0bd8d7/intel-oneapi-toolkit-2026.1.1.33_offline.sh -O intel-oneapi-toolkit_offline.sh
|
||||
sudo bash intel-oneapi-toolkit_offline.sh -s -a --silent --eula accept
|
||||
echo "Download oneAPI Deep Learning Essentials installation package"
|
||||
wget https://registrationcenter-download.intel.com/akdlm/IRC_NAS/b5c100a7-8052-4e88-90e2-63d9916a998b/intel-deep-learning-essentials-2026.1.4.17_offline.sh -O intel-deep-learning-essentials_offline.sh
|
||||
echo "Install oneAPI Deep Learning Essentials components"
|
||||
sudo bash intel-deep-learning-essentials_offline.sh -s -a --silent --eula accept
|
||||
|
||||
echo "Download oneDNN installation package"
|
||||
wget https://registrationcenter-download.intel.com/akdlm/IRC_NAS/94c3dbac-0852-45be-a57d-21c204cada3e/intel-onednn-2026.0.2.46_offline.sh -O intel-onednn_offline.sh
|
||||
echo "Install oneDNN components"
|
||||
sudo bash intel-onednn_offline.sh -s -a --silent --eula accept
|
||||
|
||||
- name: Install Level Zero SDK
|
||||
shell: bash
|
||||
@@ -79,6 +86,12 @@ jobs:
|
||||
wget -q "https://github.com/oneapi-src/level-zero/releases/download/v${LEVEL_ZERO_VERSION}/libze-dev_${LEVEL_ZERO_VERSION}%2B${LEVEL_ZERO_UBUNTU_VERSION}_amd64.deb" -O libze-dev.deb
|
||||
sudo apt-get install -y ./libze1.deb ./libze-dev.deb
|
||||
|
||||
- name: Dependencies
|
||||
id: depends
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libssl-dev
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
with:
|
||||
@@ -104,7 +117,6 @@ jobs:
|
||||
-DGGML_SYCL=ON \
|
||||
-DCMAKE_C_COMPILER=icx \
|
||||
-DCMAKE_CXX_COMPILER=icpx \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DGGML_SYCL_F16=${{ matrix.fp16 }} \
|
||||
-DGGML_SYCL_SUPPORT_LEVEL_ZERO_API=ON \
|
||||
@@ -138,8 +150,10 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
WINDOWS_BASEKIT_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/0cb67a0d-67f6-410b-868b-f4a0a17ff0cf/intel-oneapi-toolkit-2026.1.1.32_offline.exe
|
||||
WINDOWS_DPCPP_MKL: intel.oneapi.win.cpp-dpcpp-common:intel.oneapi.win.mkl.devel:intel.oneapi.win.dnnl:intel.oneapi.win.tbb.devel
|
||||
WINDOWS_DLE_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/a51104eb-6f4a-4a0e-9dae-1b81b0c81ae1/intel-deep-learning-essentials-2026.1.4.16_offline.exe
|
||||
WINDOWS_ONEDNN_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/34e28083-d04b-46a3-826c-8c1d00bf1e1c/intel-onednn-2026.0.2.47_offline.exe
|
||||
WINDOWS_DPCPP_MKL: intel.oneapi.win.cpp-dpcpp-common:intel.oneapi.win.mkl.devel:intel.oneapi.win.tbb.devel
|
||||
WINDOWS_DNNL: intel.oneapi.win.dnnl
|
||||
LEVEL_ZERO_SDK_URL: https://github.com/oneapi-src/level-zero/releases/download/v1.33.1/level-zero-win-sdk-1.33.1.zip
|
||||
ONEAPI_ROOT: "C:/Program Files (x86)/Intel/oneAPI"
|
||||
ONEAPI_INSTALLER_VERSION: "2026.1"
|
||||
@@ -151,7 +165,8 @@ jobs:
|
||||
- name: Download & Install oneAPI
|
||||
shell: bash
|
||||
run: |
|
||||
scripts/install-oneapi.bat $WINDOWS_BASEKIT_URL $WINDOWS_DPCPP_MKL
|
||||
scripts/install-oneapi.bat $WINDOWS_DLE_URL $WINDOWS_DPCPP_MKL
|
||||
scripts/install-oneapi.bat $WINDOWS_ONEDNN_URL $WINDOWS_DNNL
|
||||
|
||||
- name: Install Level Zero SDK
|
||||
shell: pwsh
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
name: CI (wasi)
|
||||
|
||||
on:
|
||||
workflow_dispatch: # allows manual triggering
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths: [
|
||||
'.github/workflows/build-wasi.yml',
|
||||
'**/CMakeLists.txt',
|
||||
'**/.cmake',
|
||||
'**/*.h',
|
||||
'**/*.hpp',
|
||||
'**/*.c',
|
||||
'**/*.cpp',
|
||||
'**/*.wgsl',
|
||||
'**/*.tmpl',
|
||||
'ggml/src/ggml-webgpu/wgsl-shaders/embed_wgsl.py'
|
||||
]
|
||||
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths: [
|
||||
'.github/workflows/build-wasi.yml',
|
||||
'**/CMakeLists.txt',
|
||||
'**/.cmake',
|
||||
'**/*.h',
|
||||
'**/*.hpp',
|
||||
'**/*.c',
|
||||
'**/*.cpp',
|
||||
'**/*.wgsl',
|
||||
'**/*.tmpl',
|
||||
'ggml/src/ggml-webgpu/wgsl-shaders/embed_wgsl.py'
|
||||
]
|
||||
|
||||
cache-mode: none
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
WASI_SDK_VERSION: 34
|
||||
DAWN_VERSION: v20260908.214631
|
||||
DAWN_COMMIT: 94c3c9cc0d5fb2e85aebb370fa8d37b71aa34655
|
||||
WASI_WEBGPU_HEADERS_COMMIT: 917765edd7cb8f8cf95bda1217514c78f71b582b
|
||||
|
||||
jobs:
|
||||
ubuntu-webgpu:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
with:
|
||||
restore: false
|
||||
save: false
|
||||
|
||||
- name: Install wasi-sdk
|
||||
uses: bytecodealliance/setup-wasi-sdk-action@77d09f3df1e8da344d8403cf8631af8977938b4c # v1.0.0
|
||||
with:
|
||||
version: ${{ env.WASI_SDK_VERSION }}
|
||||
add-to-path: false
|
||||
|
||||
- name: wasi:webgpu Dependency
|
||||
run: |
|
||||
mkdir -p wasi-webgpu/dawn-headers
|
||||
curl -fL "https://github.com/google/dawn/releases/download/${DAWN_VERSION}/dawn-headers-${DAWN_COMMIT}.tar.gz" |
|
||||
tar -xz -C wasi-webgpu/dawn-headers --strip-components=1
|
||||
|
||||
git init -q wasi-webgpu/wasi-webgpu-headers
|
||||
cd wasi-webgpu/wasi-webgpu-headers
|
||||
git fetch -q --depth 1 https://github.com/wasi-gfx/wasi-webgpu-headers.git ${WASI_WEBGPU_HEADERS_COMMIT}
|
||||
git checkout -q FETCH_HEAD
|
||||
git submodule update -q --init --depth 1
|
||||
cd ../..
|
||||
|
||||
# ggml-webgpu does find_package(Dawn) and links dawn::webgpu_dawn. There is no Dawn on WASI,
|
||||
# so this target instead compiles wasi-webgpu-headers (webgpu.h over wasi:webgpu imports)
|
||||
# and exposes Dawn's webgpu_cpp.h on top of it.
|
||||
cat > wasi-webgpu/DawnConfig.cmake << 'EOF'
|
||||
set(_wasi_webgpu "${CMAKE_CURRENT_LIST_DIR}/wasi-webgpu-headers")
|
||||
add_library(dawn::webgpu_dawn INTERFACE IMPORTED)
|
||||
set_target_properties(dawn::webgpu_dawn PROPERTIES
|
||||
INTERFACE_SOURCES "${_wasi_webgpu}/webgpu.c;${_wasi_webgpu}/async_futures.c;${_wasi_webgpu}/imports.c;${_wasi_webgpu}/imports_component_type.o"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_LIST_DIR}/dawn-headers/include/webgpu_upstream;${_wasi_webgpu}")
|
||||
EOF
|
||||
|
||||
- name: ccache-buckets-restore
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CI }}
|
||||
with:
|
||||
key: webgpu-ubuntu-24.04-arm-wasi
|
||||
folder: llama.cpp
|
||||
hf_bucket: ggml-org/cache
|
||||
|
||||
# llama-common does not build for WASI, so the executables are linked by hand
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
WASI_FLAGS="-msimd128 -D_WASI_EMULATED_SIGNAL -fwasm-exceptions -mllvm -wasm-use-legacy-eh=false"
|
||||
cmake -B build-wasi \
|
||||
-G "Ninja" \
|
||||
-DCMAKE_TOOLCHAIN_FILE=${WASI_SDK_PATH}/share/cmake/wasi-sdk-p3.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_FLAGS="${WASI_FLAGS}" \
|
||||
-DCMAKE_CXX_FLAGS="${WASI_FLAGS}" \
|
||||
-DCMAKE_HAVE_LIBC_PTHREAD=1 \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DGGML_OPENMP=OFF \
|
||||
-DGGML_WEBGPU=ON \
|
||||
-DDawn_DIR=$PWD/wasi-webgpu \
|
||||
-DLLAMA_BUILD_COMMON=OFF
|
||||
|
||||
time cmake --build build-wasi --config Release --target llama -j $(nproc)
|
||||
|
||||
LIBS="build-wasi/ggml/src/libggml.a build-wasi/ggml/src/ggml-webgpu/libggml-webgpu.a build-wasi/ggml/src/libggml-cpu.a build-wasi/ggml/src/libggml-base.a"
|
||||
LDFLAGS="-lunwind -ldl -Wl,-z,stack-size=8388608"
|
||||
CXX="${WASI_SDK_PATH}/bin/clang++ --target=wasm32-wasip3 -O3 -std=c++17 ${WASI_FLAGS} -I include -I ggml/include"
|
||||
|
||||
$CXX examples/simple/simple.cpp build-wasi/src/libllama.a $LIBS $LDFLAGS -o build-wasi/llama-simple.wasm
|
||||
$CXX tests/test-backend-ops.cpp $LIBS $LDFLAGS -o build-wasi/test-backend-ops.wasm
|
||||
|
||||
- name: ccache-buckets-save
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CACHE_OUTPUT }}
|
||||
with:
|
||||
key: webgpu-ubuntu-24.04-arm-wasi
|
||||
folder: llama.cpp
|
||||
evict-old-files: 1d
|
||||
hf_bucket: ggml-org/cache
|
||||
save: true
|
||||
@@ -7,15 +7,7 @@ on:
|
||||
- master
|
||||
paths: [
|
||||
'.github/workflows/build-webgpu.yml',
|
||||
'**/CMakeLists.txt',
|
||||
'**/.cmake',
|
||||
'**/*.h',
|
||||
'**/*.hpp',
|
||||
'**/*.c',
|
||||
'**/*.cpp',
|
||||
'**/*.wgsl',
|
||||
'**/*.tmpl',
|
||||
'ggml/src/ggml-webgpu/wgsl-shaders/embed_wgsl.py'
|
||||
'ggml/src/ggml-webgpu/**'
|
||||
]
|
||||
|
||||
pull_request:
|
||||
@@ -63,135 +55,3 @@ jobs:
|
||||
-type f \( -name '*.cpp' -o -name '*.hpp' -o -name '*.h' \) \
|
||||
-print0 |
|
||||
xargs -0 clang-format-22 --dry-run --Werror
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
with:
|
||||
restore: false
|
||||
save: false
|
||||
|
||||
- name: Dawn Dependency
|
||||
id: dawn-depends
|
||||
run: |
|
||||
DAWN_VERSION="v20260908.214631"
|
||||
DAWN_OWNER="google"
|
||||
DAWN_REPO="dawn"
|
||||
DAWN_ASSET_NAME="Dawn-94c3c9cc0d5fb2e85aebb370fa8d37b71aa34655-macos-latest-Release"
|
||||
echo "Fetching release asset from https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz"
|
||||
curl -L -o artifact.tar.gz \
|
||||
"https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz"
|
||||
mkdir dawn
|
||||
tar -xvf artifact.tar.gz -C dawn --strip-components=1
|
||||
|
||||
- name: ccache-buckets-restore
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CI }}
|
||||
with:
|
||||
key: webgpu-macos-latest
|
||||
folder: llama.cpp
|
||||
hf_bucket: ggml-org/cache
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
export CMAKE_PREFIX_PATH=dawn
|
||||
cmake -B build -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DGGML_WEBGPU=ON -DGGML_METAL=OFF -DGGML_BLAS=OFF
|
||||
time cmake --build build --config Release -j $(sysctl -n hw.logicalcpu)
|
||||
|
||||
- name: ccache-buckets-save
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CACHE_OUTPUT }}
|
||||
with:
|
||||
key: webgpu-macos-latest
|
||||
folder: llama.cpp
|
||||
evict-old-files: 1d
|
||||
hf_bucket: ggml-org/cache
|
||||
save: true
|
||||
|
||||
- name: Test
|
||||
id: cmake_test
|
||||
run: |
|
||||
cd build
|
||||
ctest -L main --verbose --timeout 900
|
||||
|
||||
ubuntu:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
with:
|
||||
restore: false
|
||||
save: false
|
||||
|
||||
- name: Dependencies
|
||||
id: depends
|
||||
run: |
|
||||
sudo add-apt-repository -y ppa:kisak/kisak-mesa
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y build-essential mesa-vulkan-drivers \
|
||||
libxcb-xinput0 libxcb-xinerama0 libxcb-cursor-dev libssl-dev
|
||||
|
||||
- name: Dawn Dependency
|
||||
id: dawn-depends
|
||||
run: |
|
||||
sudo apt-get install -y libxrandr-dev libxinerama-dev libxcursor-dev mesa-common-dev libx11-xcb-dev libxi-dev
|
||||
DAWN_VERSION="v20260908.214631"
|
||||
DAWN_OWNER="google"
|
||||
DAWN_REPO="dawn"
|
||||
DAWN_ASSET_NAME="Dawn-94c3c9cc0d5fb2e85aebb370fa8d37b71aa34655-ubuntu-latest-Release"
|
||||
echo "Fetching release asset from https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz"
|
||||
curl -L -o artifact.tar.gz \
|
||||
"https://github.com/google/dawn/releases/download/${DAWN_VERSION}/${DAWN_ASSET_NAME}.tar.gz"
|
||||
mkdir dawn
|
||||
tar -xvf artifact.tar.gz -C dawn --strip-components=1
|
||||
|
||||
- name: ccache-buckets-restore
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CI }}
|
||||
with:
|
||||
key: webgpu-ubuntu-24.04
|
||||
folder: llama.cpp
|
||||
hf_bucket: ggml-org/cache
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
export Dawn_DIR=dawn/lib64/cmake/Dawn
|
||||
cmake -B build \
|
||||
-DGGML_WEBGPU=ON
|
||||
time cmake --build build --config Release -j $(nproc)
|
||||
|
||||
- name: ccache-buckets-save
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
uses: ./.github/actions/ccache-buckets
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN_CACHE_OUTPUT }}
|
||||
with:
|
||||
key: webgpu-ubuntu-24.04
|
||||
folder: llama.cpp
|
||||
evict-old-files: 1d
|
||||
hf_bucket: ggml-org/cache
|
||||
save: true
|
||||
|
||||
- name: Test
|
||||
id: cmake_test
|
||||
run: |
|
||||
cd build
|
||||
ctest -L main --verbose --timeout 900
|
||||
|
||||
@@ -46,7 +46,7 @@ env:
|
||||
|
||||
jobs:
|
||||
cpu-x64-high-perf:
|
||||
runs-on: [self-hosted, Linux, X64]
|
||||
runs-on: [self-hosted, Linux, X64, CPU]
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Test
|
||||
id: ggml-ci
|
||||
run: |
|
||||
LLAMA_ARG_THREADS=$(nproc) GG_BUILD_HIGH_PERF=1 GG_BUILD_EXTRA_TESTS_0=1 bash ./ci/run.sh ~/results/llama.cpp ~/mnt/llama.cpp
|
||||
LLAMA_ARG_THREADS=$(nproc) GG_BUILD_HIGH_PERF=1 GG_BUILD_EXTRA_TESTS_0=1 bash ./ci/run.sh ~/results/llama.cpp/$RUNNER_NAME ~/mnt/llama.cpp
|
||||
|
||||
cpu-arm64-high-perf-graviton4:
|
||||
runs-on: ah-ubuntu_24_04-c8g_8x
|
||||
|
||||
@@ -60,4 +60,4 @@ jobs:
|
||||
- name: Test
|
||||
id: ggml-ci
|
||||
run: |
|
||||
GG_BUILD_METAL=1 bash ./ci/run.sh ~/results/llama.cpp ~/mnt/llama.cpp
|
||||
GG_BUILD_METAL=1 bash ./ci/run.sh ~/results/llama.cpp/$RUNNER_NAME ~/mnt/llama.cpp
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
id: ggml-ci
|
||||
run: |
|
||||
vulkaninfo --summary
|
||||
GG_BUILD_VULKAN=1 GGML_VK_DISABLE_COOPMAT2=1 bash ./ci/run.sh ~/results/llama.cpp ~/mnt/llama.cpp
|
||||
GG_BUILD_VULKAN=1 GGML_VK_DISABLE_COOPMAT2=1 bash ./ci/run.sh ~/results/llama.cpp/$RUNNER_NAME ~/mnt/llama.cpp
|
||||
|
||||
# - name: ccache-buckets-save
|
||||
# if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
id: ggml-ci
|
||||
run: |
|
||||
vulkaninfo --summary
|
||||
GG_BUILD_VULKAN=1 bash ./ci/run.sh ~/results/llama.cpp ~/mnt/llama.cpp
|
||||
GG_BUILD_VULKAN=1 bash ./ci/run.sh ~/results/llama.cpp/$RUNNER_NAME ~/mnt/llama.cpp
|
||||
|
||||
# - name: ccache-buckets-save
|
||||
# if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
id: ggml-ci
|
||||
run: |
|
||||
vulkaninfo --summary
|
||||
GG_BUILD_VULKAN=1 bash ./ci/run.sh ~/results/llama.cpp ~/mnt/llama.cpp
|
||||
GG_BUILD_VULKAN=1 bash ./ci/run.sh ~/results/llama.cpp/$RUNNER_NAME ~/mnt/llama.cpp
|
||||
|
||||
gpu-vulkan-intel-linux:
|
||||
runs-on: [self-hosted, Linux, Intel]
|
||||
|
||||
@@ -58,7 +58,12 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y build-essential cmake libxcb-xinput0 libxcb-xinerama0 libxcb-cursor-dev libvulkan1 mesa-vulkan-drivers libglvnd0 libgl1 libglx0 libegl1 libgles2 libssl-dev time unzip wget python3 python3-venv python3-pip
|
||||
sudo apt install -y software-properties-common
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt update
|
||||
sudo apt install -y build-essential cmake libxcb-xinput0 libxcb-xinerama0 libxcb-cursor-dev libvulkan1 mesa-vulkan-drivers libglvnd0 libgl1 libglx0 libegl1 libgles2 libssl-dev time unzip wget python3.12 python3.12-venv
|
||||
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1
|
||||
sudo update-alternatives --config python3
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
@@ -131,4 +136,4 @@ jobs:
|
||||
id: ggml-ci
|
||||
run: |
|
||||
GG_BUILD_WEBGPU=1 GG_BUILD_WEBGPU_DAWN_PREFIX="$GITHUB_WORKSPACE/dawn" \
|
||||
bash ./ci/run.sh ~/results/llama.cpp ~/mnt/llama.cpp
|
||||
bash ./ci/run.sh ~/results/llama.cpp/$RUNNER_NAME ~/mnt/llama.cpp
|
||||
|
||||
@@ -61,8 +61,17 @@ jobs:
|
||||
RELEASE_BRANCH: ${{ github.ref_name }}
|
||||
SKIP_APIABI_CHECK: ${{ github.event.inputs.skip_apiabi_check }}
|
||||
APIABI_COMPARE_TAG: ${{ github.event.inputs.apiabi_compare_tag }}
|
||||
APIABI_REPORT_DIR: ${{ runner.temp }}/apiabi-reports
|
||||
REQUIRE_DOCKER: ${{ github.event.inputs.require_docker }}
|
||||
|
||||
- name: Upload API/ABI reports
|
||||
if: ${{ always() && github.event.inputs.skip_apiabi_check != 'true' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: apiabi-reports
|
||||
path: ${{ runner.temp }}/apiabi-reports
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Create release tag
|
||||
if: ${{ github.event.inputs.dry_run == 'false' }}
|
||||
run: |
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
pip-install: -r requirements/requirements-all.txt ty==0.0.84
|
||||
pip-install: -r requirements/requirements-all.txt ty==0.0.88
|
||||
# - name: Type-check with Pyright
|
||||
# uses: jakebailey/pyright-action@v2
|
||||
# with:
|
||||
|
||||
@@ -172,6 +172,8 @@ jobs:
|
||||
- [Ubuntu x64 (CPU)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-x64.tar.gz)
|
||||
- [Ubuntu arm64 (CPU)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-arm64.tar.gz)
|
||||
- [Ubuntu s390x (CPU)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-s390x.tar.gz)
|
||||
- [Ubuntu 26.04 s390x (CPU)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-26.04-s390x.tar.gz)
|
||||
- [Ubuntu 26.04 s390x (zDNN)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-zdnn-s390x.tar.gz)
|
||||
- [Ubuntu x64 (Vulkan)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-vulkan-x64.tar.gz)
|
||||
- [Ubuntu arm64 (Vulkan)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-vulkan-arm64.tar.gz)
|
||||
- [Ubuntu x64 (CUDA 12)](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-12.8-x64.tar.gz) - [CUDA 12.8 libraries](https://github.com/ggml-org/llama.cpp/releases/download/${{ steps.tag.outputs.name }}/cudart-llama-${{ steps.tag.outputs.name }}-bin-ubuntu-cuda-12.8-x64.tar.gz)
|
||||
|
||||
@@ -35,7 +35,7 @@ permissions:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
CMAKE_ARGS: "-DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_TOOLS=ON -DLLAMA_BUILD_SERVER=ON -DGGML_RPC=ON"
|
||||
CMAKE_ARGS: "-DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_TOOLS=ON -DLLAMA_BUILD_SERVER=ON -DGGML_RPC=ON -DLLAMA_CONNECT=ON"
|
||||
|
||||
# note: run this workflow one at a time for better cache reuse
|
||||
concurrency:
|
||||
@@ -180,6 +180,8 @@ jobs:
|
||||
os: ubuntu-24.04-arm
|
||||
- build: 's390x'
|
||||
os: ubuntu-24.04-s390x
|
||||
- build: '26.04-s390x'
|
||||
os: ubuntu-26.04-s390x
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -200,7 +202,7 @@ jobs:
|
||||
id: depends
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install build-essential libssl-dev
|
||||
sudo apt-get install -y build-essential libssl-dev
|
||||
|
||||
- name: Toolchain workaround (GCC 14)
|
||||
if: ${{ contains(matrix.os, 'ubuntu-24.04') }}
|
||||
@@ -210,7 +212,7 @@ jobs:
|
||||
echo "CXX=g++-14" >> "$GITHUB_ENV"
|
||||
|
||||
- name: ccache
|
||||
if: ${{ matrix.build != 's390x' }}
|
||||
if: ${{ !contains(matrix.os, 's390x') }}
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
with:
|
||||
key: release-${{ matrix.os }}-cpu
|
||||
@@ -242,7 +244,7 @@ jobs:
|
||||
archive: false
|
||||
|
||||
- name: ccache-clear
|
||||
if: ${{ matrix.build != 's390x' }}
|
||||
if: ${{ !contains(matrix.os, 's390x') }}
|
||||
uses: ./.github/actions/ccache-clear
|
||||
with:
|
||||
key: release-${{ matrix.os }}-cpu
|
||||
@@ -547,7 +549,7 @@ jobs:
|
||||
if: ${{ needs.check-release.outputs.should_release == 'true' }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
container: 'ghcr.io/snapdragon-toolchain/arm64-android:v0.7'
|
||||
container: 'ghcr.io/snapdragon-toolchain/arm64-android:v0.8'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -600,7 +602,7 @@ jobs:
|
||||
if: ${{ needs.check-release.outputs.should_release == 'true' }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
container: 'ghcr.io/snapdragon-toolchain/arm64-linux:v0.7'
|
||||
container: 'ghcr.io/snapdragon-toolchain/arm64-linux:v0.8'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -968,6 +970,11 @@ jobs:
|
||||
evict-old-files: 1d
|
||||
max-size: "1G"
|
||||
|
||||
- name: Tune ccache for ROCm
|
||||
run: |
|
||||
# since ROCm 10.1, clang -E output has no GPU code, so the preprocessor fallback can reuse objects with old GPU code
|
||||
ccache --set-config=depend_mode=true
|
||||
|
||||
# - name: Cache ROCm Installation
|
||||
# id: cache-rocm
|
||||
# uses: actions/cache@v5
|
||||
@@ -1300,8 +1307,11 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
WINDOWS_BASEKIT_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/0cb67a0d-67f6-410b-868b-f4a0a17ff0cf/intel-oneapi-toolkit-2026.1.1.32_offline.exe
|
||||
WINDOWS_DPCPP_MKL: intel.oneapi.win.cpp-dpcpp-common:intel.oneapi.win.mkl.devel:intel.oneapi.win.dnnl:intel.oneapi.win.tbb.devel
|
||||
WINDOWS_DLE_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/a51104eb-6f4a-4a0e-9dae-1b81b0c81ae1/intel-deep-learning-essentials-2026.1.4.16_offline.exe
|
||||
WINDOWS_ONEDNN_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/34e28083-d04b-46a3-826c-8c1d00bf1e1c/intel-onednn-2026.0.2.47_offline.exe
|
||||
WINDOWS_DPCPP_MKL: intel.oneapi.win.cpp-dpcpp-common:intel.oneapi.win.mkl.devel:intel.oneapi.win.tbb.devel
|
||||
WINDOWS_DNNL: intel.oneapi.win.dnnl
|
||||
|
||||
LEVEL_ZERO_SDK_URL: https://github.com/oneapi-src/level-zero/releases/download/v1.33.1/level-zero-win-sdk-1.33.1.zip
|
||||
ONEAPI_ROOT: "C:/Program Files (x86)/Intel/oneAPI"
|
||||
ONEAPI_INSTALLER_VERSION: "2026.1"
|
||||
@@ -1314,7 +1324,8 @@ jobs:
|
||||
- name: Download & Install oneAPI
|
||||
shell: bash
|
||||
run: |
|
||||
scripts/install-oneapi.bat $WINDOWS_BASEKIT_URL $WINDOWS_DPCPP_MKL
|
||||
scripts/install-oneapi.bat $WINDOWS_DLE_URL $WINDOWS_DPCPP_MKL
|
||||
scripts/install-oneapi.bat $WINDOWS_ONEDNN_URL $WINDOWS_DNNL
|
||||
|
||||
- name: Install Level Zero SDK
|
||||
shell: pwsh
|
||||
@@ -1425,8 +1436,15 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cd /tmp
|
||||
wget https://registrationcenter-download.intel.com/akdlm/IRC_NAS/5996e26b-f48a-42b1-8db0-b002ad0bd8d7/intel-oneapi-toolkit-2026.1.1.33_offline.sh -O intel-oneapi-toolkit_offline.sh
|
||||
sudo bash intel-oneapi-toolkit_offline.sh -s -a --silent --eula accept
|
||||
echo "Download oneAPI Deep Learning Essentials installation package"
|
||||
wget https://registrationcenter-download.intel.com/akdlm/IRC_NAS/b5c100a7-8052-4e88-90e2-63d9916a998b/intel-deep-learning-essentials-2026.1.4.17_offline.sh -O intel-deep-learning-essentials_offline.sh
|
||||
echo "Install oneAPI Deep Learning Essentials components"
|
||||
sudo bash intel-deep-learning-essentials_offline.sh -s -a --silent --eula accept
|
||||
|
||||
echo "Download oneDNN installation package"
|
||||
wget https://registrationcenter-download.intel.com/akdlm/IRC_NAS/94c3dbac-0852-45be-a57d-21c204cada3e/intel-onednn-2026.0.2.46_offline.sh -O intel-onednn_offline.sh
|
||||
echo "Install oneDNN components"
|
||||
sudo bash intel-onednn_offline.sh -s -a --silent --eula accept
|
||||
|
||||
- name: Install Level Zero SDK
|
||||
shell: bash
|
||||
@@ -1442,6 +1460,12 @@ jobs:
|
||||
name: llama-ui.zip
|
||||
path: tools/ui/dist
|
||||
|
||||
- name: Dependencies
|
||||
id: depends
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libssl-dev
|
||||
|
||||
- name: ccache
|
||||
uses: ggml-org/ccache-action@v1.2.24
|
||||
with:
|
||||
@@ -1460,8 +1484,8 @@ jobs:
|
||||
-DCMAKE_CXX_COMPILER=icpx \
|
||||
-DCMAKE_INSTALL_RPATH='$ORIGIN' \
|
||||
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DLLAMA_CONNECT=ON \
|
||||
-DGGML_SYCL_F16=${{ matrix.fp16 }}
|
||||
time cmake --build build --config Release -j $(nproc)
|
||||
|
||||
@@ -1528,6 +1552,8 @@ jobs:
|
||||
# builds hit.
|
||||
ccache --set-config=compiler_check=content
|
||||
ccache --set-config=sloppiness=time_macros,include_file_mtime,include_file_ctime
|
||||
# since ROCm 10.1, clang -E output has no GPU code, so the preprocessor fallback can reuse objects with old GPU code
|
||||
ccache --set-config=depend_mode=true
|
||||
|
||||
- name: Dependencies
|
||||
id: depends
|
||||
@@ -1602,6 +1628,60 @@ jobs:
|
||||
with:
|
||||
key: release-ubuntu-24.04-rocm-${{ matrix.ROCM_VERSION }}-${{ matrix.build }}
|
||||
|
||||
ubuntu-26-zdnn:
|
||||
needs: [check-release, ui-build]
|
||||
if: ${{ needs.check-release.outputs.should_release == 'true' }}
|
||||
|
||||
runs-on: ubuntu-26.04-s390x
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download UI build
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: llama-ui.zip
|
||||
path: tools/ui/dist
|
||||
|
||||
- name: Dependencies
|
||||
id: depends
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
build-essential libssl-dev libzdnn-dev
|
||||
|
||||
- name: Build
|
||||
id: cmake_build
|
||||
run: |
|
||||
cmake -B build \
|
||||
-DCMAKE_INSTALL_RPATH='$ORIGIN' \
|
||||
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
|
||||
-DGGML_BACKEND_DL=ON \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DGGML_VXE=ON \
|
||||
-DGGML_ZDNN=ON \
|
||||
-DCMAKE_C_FLAGS="-march=arch15" \
|
||||
-DCMAKE_CXX_FLAGS="-march=arch15" \
|
||||
-DLLAMA_FATAL_WARNINGS=ON \
|
||||
${{ env.CMAKE_ARGS }}
|
||||
cmake --build build --config Release -j $(nproc)
|
||||
|
||||
- name: Pack artifacts
|
||||
id: pack_artifacts
|
||||
run: |
|
||||
cp LICENSE ./build/bin/
|
||||
tar -czvf llama-${{ needs.check-release.outputs.tag_name }}-bin-ubuntu-zdnn-s390x.tar.gz --transform "s,^\.,llama-${{ needs.check-release.outputs.tag_name }}," -C ./build/bin .
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: llama-${{ needs.check-release.outputs.tag_name }}-bin-ubuntu-zdnn-s390x.tar.gz
|
||||
archive: false
|
||||
|
||||
ios-xcode:
|
||||
needs: [check-release]
|
||||
if: ${{ needs.check-release.outputs.should_release == 'true' }}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
Project-specific instructions:
|
||||
|
||||
## Coding
|
||||
|
||||
- In `test-backend-ops.cpp`, do not mention specific backends (e.g. Metal, CUDA) in comments
|
||||
@@ -1,33 +0,0 @@
|
||||
You are a coding agent. Here are some very important rules that you must follow:
|
||||
|
||||
General:
|
||||
- Be very precise and concise when writing code, comments, explanations, etc.
|
||||
- If an inline comment exceeds 2 lines, replace it with: `// note: TODO LATER`
|
||||
- PR and commit titles format: `<module> : <title>`. Lookup recents for examples
|
||||
- Don't try to build or run the code unless you are explicitly asked to do so
|
||||
- Use the `gh` CLI tool when querying PRs, issues, or other GitHub resources
|
||||
- When [MODEL] is needed, first try to get it from the `PI_MODEL_NAME` env var before asking the user
|
||||
- Never read the `AGENTS.md` file
|
||||
|
||||
Coding:
|
||||
- When in doubt, always refer to the CONTRIBUTING.md file of the project
|
||||
- In `test-backend-ops.cpp`, do not mention specific backends (e.g. Metal, CUDA) in comments
|
||||
- When referencing issues or PRs in comments, use the format:
|
||||
- C/C++ code: `// ref: <url>`
|
||||
- Other (CMake, etc.): `# ref: <url>`
|
||||
|
||||
Pull requests (PRs):
|
||||
- New branch names are prefixed with "gg/"
|
||||
- Before opening a pull request, ask the user to confirm the description
|
||||
- Don't explicitly wrap lines in the PR description (each paragraph and bullet is a single line)
|
||||
- When creating a pull request, look for the repository's PR template and follow it
|
||||
- For the AI usage disclosure section, write "YES. pi:llama.cpp/[MODEL]"
|
||||
- If `PI_MODEL_NAME` env var is not set, ask the user to tell you what model was used and write it in place of [MODEL]
|
||||
- Always create the pull requests in draft mode
|
||||
- Never reply to review comments or post comments on issues/PRs without explicit permission from the user
|
||||
|
||||
Commits:
|
||||
- On every commit that you make, include a "Assisted-by: pi:llama.cpp/[MODEL]" tag
|
||||
- Do not explicitly set the git author in commits - rely on the default git config
|
||||
- Always use `--no-gpg-sign` when committing
|
||||
- Never `git push` without explicit confirmation from the user
|
||||
@@ -246,3 +246,6 @@ Chat template and parser:
|
||||
- [PEG parser](docs/development/parsing.md) - alternative to regex that llama.cpp uses to parse model's output
|
||||
- [Auto parser](docs/autoparser.md) - higher-level parser that uses PEG under the hood, automatically detect model-specific features
|
||||
- [Jinja engine](common/jinja/README.md)
|
||||
|
||||
Testing:
|
||||
- [Slice model](skills/slice-model/SKILL.md) - build a small slice (few layers/experts) of a big HF model without downloading the full weights, to test conversion and inference
|
||||
|
||||
+6
-2
@@ -4,7 +4,7 @@ include(CheckIncludeFileCXX)
|
||||
|
||||
### llama.cpp version
|
||||
set(LLAMA_VERSION_MAJOR 0)
|
||||
set(LLAMA_VERSION_MINOR 6)
|
||||
set(LLAMA_VERSION_MINOR 8)
|
||||
set(LLAMA_VERSION_PATCH 0)
|
||||
set(LLAMA_VERSION_BASE "${LLAMA_VERSION_MAJOR}.${LLAMA_VERSION_MINOR}.${LLAMA_VERSION_PATCH}")
|
||||
|
||||
@@ -103,7 +103,7 @@ endif()
|
||||
|
||||
# subprocess spawning isn't a supported/sandbox-friendly operation on mobile OSes or in WASM
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "iOS" OR CMAKE_SYSTEM_NAME STREQUAL "Android" OR ANDROID
|
||||
OR CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR EMSCRIPTEN)
|
||||
OR CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR EMSCRIPTEN OR CMAKE_SYSTEM_NAME STREQUAL "WASI")
|
||||
set(LLAMA_SUBPROCESS_DEFAULT OFF)
|
||||
else()
|
||||
set(LLAMA_SUBPROCESS_DEFAULT ON)
|
||||
@@ -136,6 +136,7 @@ option(LLAMA_BUILD_SERVER "llama: build server example"
|
||||
option(LLAMA_BUILD_APP "llama: build the unified binary" ${LLAMA_STANDALONE})
|
||||
option(LLAMA_BUILD_UI "llama: build the embedded Web UI for server" OFF)
|
||||
option(LLAMA_USE_PREBUILT_UI "llama: use prebuilt UI from HF Bucket when available" ON)
|
||||
option(LLAMA_CONNECT "llama: include the llama-connect tool (downloads a prebuilt binary)" OFF)
|
||||
|
||||
option(LLAMA_TOOLS_INSTALL "llama: install tools" ${LLAMA_TOOLS_INSTALL_DEFAULT})
|
||||
option(LLAMA_TESTS_INSTALL "llama: install tests" ON)
|
||||
@@ -149,6 +150,7 @@ option(LLAMA_LLGUIDANCE "llama-common: include LLGuidance library for structured
|
||||
# Required for relocatable CMake package
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/build-info.cmake)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/common.cmake)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/win-app.cmake)
|
||||
|
||||
if (NOT DEFINED LLAMA_BUILD_NUMBER)
|
||||
set(LLAMA_BUILD_NUMBER ${BUILD_NUMBER})
|
||||
@@ -268,6 +270,8 @@ if (LLAMA_BUILD_MTMD AND NOT (LLAMA_BUILD_COMMON AND LLAMA_BUILD_TOOLS))
|
||||
add_subdirectory(tools/mtmd)
|
||||
endif()
|
||||
|
||||
llama_apply_app_manifests()
|
||||
|
||||
#
|
||||
# install
|
||||
#
|
||||
|
||||
+22
-29
@@ -2,20 +2,29 @@
|
||||
# multiple collaborators per item can be specified
|
||||
#
|
||||
# ggml-org/ci : CISC, danbev, ggerganov, netrunnereve, ngxson, taronaeo
|
||||
# ggml-org/ggml-base : aendk, fairydreaming, ggerganov
|
||||
# ggml-org/ggml-cann : hipudding
|
||||
# ggml-org/ggml-cuda : JohannesGaessler, am17an, IMbackK, ORippler
|
||||
# ggml-org/ggml-cpu : angt, chaxu01, ggerganov, max-krasnyansky, taronaeo
|
||||
# ggml-org/ggml-cuda : am17an, gaugarg-nv, IMbackK, JohannesGaessler, ORippler, thelittlefireman, ynankani
|
||||
# ggml-org/ggml-et : marty1885
|
||||
# ggml-org/ggml-hexagon : lhez, max-krasnyansky
|
||||
# ggml-org/ggml-metal : ggerganov
|
||||
# ggml-org/ggml-hip : IMbackK, thelittlefireman
|
||||
# ggml-org/ggml-metal : forforever73, ggerganov, jhen0409, masterFoad
|
||||
# ggml-org/ggml-opencl : lhez, max-krasnyansky
|
||||
# ggml-org/ggml-rpc : rgerganov
|
||||
# ggml-org/ggml-sycl : arthw
|
||||
# ggml-org/ggml-openvino : ravi9, wine99
|
||||
# ggml-org/ggml-riscv : xctan
|
||||
# ggml-org/ggml-rpc : am17an, rgerganov
|
||||
# ggml-org/ggml-sycl : arthw, Titaniumtown
|
||||
# ggml-org/ggml-vulkan : 0cc4m, jeffbolznv
|
||||
# ggml-org/ggml-webgpu : reeselevine, yomaytk
|
||||
# ggml-org/ggml-zdnn : taronaeo
|
||||
# ggml-org/llama-common : ggerganov, aldehir, angt, danbev, ngxson, pwilkin
|
||||
# ggml-org/ggml-zdnn : AlekseiNikiforovIBM, Andreas-Krebbel, taronaeo
|
||||
# ggml-org/llama-base : am17an, CISC, ggerganov, ngxson, sanmai, ServeurpersoCom
|
||||
# ggml-org/llama-common : aldehir, angt, bartowski1182, danbev, ggerganov, ngxson, nikwen, pwilkin, ruixiang63, sanmai
|
||||
# ggml-org/llama-examples : ggerganov
|
||||
# ggml-org/llama-models : CISC, danbev, ggerganov, ngxson
|
||||
# ggml-org/llama-mtmd : ngxson
|
||||
# ggml-org/llama-server : ggerganov, ngxson, allozaur, angt, ServeurpersoCom
|
||||
# ggml-org/llama-ui : allozaur
|
||||
# ggml-org/llama-server : allozaur, angt, ggerganov, ngxson, ServeurpersoCom
|
||||
# ggml-org/llama-ui : allozaur
|
||||
|
||||
/.devops/*.Dockerfile @ngxson
|
||||
/.github/actions/ @ggml-org/ci
|
||||
@@ -29,28 +38,12 @@
|
||||
/conversion/ @CISC
|
||||
/convert_*.py @CISC
|
||||
/docs/backend/snapdragon/ @ggml-org/ggml-hexagon
|
||||
/examples/batched.swift/ @ggerganov
|
||||
/examples/batched/ @ggerganov
|
||||
/examples/convert-llama2c-to-ggml/ @ggerganov
|
||||
/examples/ @ggml-org/llama-examples
|
||||
/examples/debug/ @danbev @pwilkin
|
||||
/examples/deprecation-warning/ @ggerganov
|
||||
/examples/diffusion/ @am17an
|
||||
/examples/embedding/ @ggerganov
|
||||
/examples/eval-callback/ @ggerganov
|
||||
/examples/export-docs/ @ggerganov
|
||||
/examples/gen-docs/ @ggerganov
|
||||
/examples/gguf/ @ggerganov
|
||||
/examples/llama.android/ @ggerganov @hanyin-arm @naco-siren
|
||||
/examples/llama.swiftui/ @ggerganov
|
||||
/examples/llama.vim @ggerganov
|
||||
/examples/lookahead/ @ggerganov
|
||||
/examples/lookup/ @JohannesGaessler
|
||||
/examples/model-conversion/ @danbev
|
||||
/examples/parallel/ @ggerganov
|
||||
/examples/passkey/ @ggerganov
|
||||
/examples/retrieval/ @ggerganov
|
||||
/examples/speculative-simple/ @ggerganov
|
||||
/examples/speculative/ @ggerganov
|
||||
/ggml/cmake/ @ggerganov
|
||||
/ggml/include/ @ggerganov
|
||||
/ggml/src/ggml-backend-meta.cpp @JohannesGaessler
|
||||
@@ -62,7 +55,7 @@
|
||||
/ggml/src/ggml-cuda/ @ggml-org/ggml-cuda
|
||||
/ggml/src/ggml-cuda/vendors/hip.h @IMbackK
|
||||
/ggml/src/ggml-hexagon/ @ggml-org/ggml-hexagon
|
||||
/ggml/src/ggml-hip/ @IMbackK
|
||||
/ggml/src/ggml-hip/ @ggml-org/ggml-hip
|
||||
/ggml/src/ggml-et/ @marty1885
|
||||
/ggml/src/ggml-impl.h @ggerganov
|
||||
/ggml/src/ggml-metal/ @ggml-org/ggml-metal
|
||||
@@ -76,7 +69,7 @@
|
||||
/ggml/src/ggml-virtgpu/ @kpouget
|
||||
/ggml/src/ggml-vulkan/ @ggml-org/ggml-vulkan
|
||||
/ggml/src/ggml-webgpu/ @ggml-org/ggml-webgpu
|
||||
/ggml/src/ggml-zdnn/ @ggml-org/ggml-zdnn @Andreas-Krebbel @AlekseiNikiforovIBM
|
||||
/ggml/src/ggml-zdnn/ @ggml-org/ggml-zdnn
|
||||
/ggml/src/ggml-zendnn/ @avinashcpandey @Jiten1parmar
|
||||
/ggml/src/ggml.c @ggerganov
|
||||
/ggml/src/ggml.cpp @ggerganov
|
||||
@@ -87,14 +80,14 @@
|
||||
/scripts/get* @ggerganov
|
||||
/scripts/sync* @ggerganov
|
||||
/scripts/snapdragon/ @ggml-org/ggml-hexagon
|
||||
/src/ @ggerganov
|
||||
/src/ @ggml-org/llama-base
|
||||
/src/llama-adapter.* @CISC
|
||||
/src/llama-arch.* @CISC
|
||||
/src/llama-chat.* @ngxson
|
||||
/src/llama-graph.* @CISC
|
||||
/src/llama-model.* @CISC
|
||||
/src/llama-vocab.* @CISC
|
||||
/src/models/ @CISC
|
||||
/src/models/ @ggml-org/llama-models
|
||||
/tests/ @ggerganov
|
||||
/tests/test-chat.* @pwilkin
|
||||
/tests/test-server-component.cpp @ggml-org/llama-server
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[](https://github.com/ggml-org/llama.cpp/actions/workflows/docker.yml)
|
||||
[](https://github.com/ggml-org/llama.cpp/actions/workflows/winget.yml)
|
||||
|
||||
[ggml](https://github.com/ggml-org/ggml) / [ops](https://github.com/ggml-org/llama.cpp/blob/master/docs/ops.md) / [maintainer PRs](https://github.com/ggml-org/llama.cpp/issues?q=is%3Apr%20is%3Aopen%20draft%3AFalse%20(author%3Argerganov%20OR%20author%3AKitaitiMakoto%20OR%20author%3Adanbev%20OR%20author%3Aaldehir%20OR%20author%3Amax-krasnyansky%20OR%20author%3ACISC%20OR%20author%3Aggerganov%20OR%20author%3Aam17an%20OR%20author%3Ajhen0409%20OR%20author%3Abartowski1182%20OR%20author%3Anikwen%20OR%20author%3Ahipudding%20OR%20author%3Aravi9%20OR%20author%3AServeurpersoCom%20OR%20author%3Apwilkin%20OR%20author%3Areeselevine%20OR%20author%3Angxson%20OR%20author%3Ajeffbolznv%20OR%20author%3Amarty1885%20OR%20author%3A0cc4m%20OR%20author%3ATitaniumtown%20OR%20author%3Aangt%20OR%20author%3AIMbackK%20OR%20author%3Aarthw%20OR%20author%3AJohannesGaessler%20OR%20author%3AORippler%20OR%20author%3Aruixiang63%20OR%20author%3Axctan%20OR%20author%3Aallozaur%20OR%20author%3Ayomaytk%20OR%20author%3Aaendk%20OR%20author%3Awine99%20OR%20author%3Agaugarg-nv%20OR%20author%3Ataronaeo%20OR%20author%3Aforforever73%20OR%20author%3Alhez%20OR%20author%3Anetrunnereve%20OR%20author%3Afairydreaming)%20sort%3Aupdated-desc) / [dev stats](https://github.com/ggml-org/llama.cpp-dev) / [lib llama API](https://github.com/ggml-org/llama.cpp/issues/9289) / [llama-server REST API](https://github.com/ggml-org/llama.cpp/issues/9291)
|
||||
[ggml](https://github.com/ggml-org/ggml) / [ops](https://github.com/ggml-org/llama.cpp/blob/master/docs/ops.md) / [maintainer PRs](https://github.com/ggml-org/llama.cpp/issues?q=is%3Apr%20is%3Aopen%20draft%3AFalse%20(author%3Asanmai%20OR%20author%3Argerganov%20OR%20author%3AKitaitiMakoto%20OR%20author%3Adanbev%20OR%20author%3Aaldehir%20OR%20author%3Amax-krasnyansky%20OR%20author%3ACISC%20OR%20author%3Aggerganov%20OR%20author%3Aam17an%20OR%20author%3Ajhen0409%20OR%20author%3Abartowski1182%20OR%20author%3Anikwen%20OR%20author%3Ahipudding%20OR%20author%3Aravi9%20OR%20author%3AServeurpersoCom%20OR%20author%3Athelittlefireman%20OR%20author%3Apwilkin%20OR%20author%3Areeselevine%20OR%20author%3Angxson%20OR%20author%3Ajeffbolznv%20OR%20author%3Amarty1885%20OR%20author%3A0cc4m%20OR%20author%3ATitaniumtown%20OR%20author%3Aangt%20OR%20author%3AIMbackK%20OR%20author%3Aarthw%20OR%20author%3AJohannesGaessler%20OR%20author%3AORippler%20OR%20author%3Aruixiang63%20OR%20author%3Axctan%20OR%20author%3AmasterFoad%20OR%20author%3Aallozaur%20OR%20author%3AAndreas-Krebbel%20OR%20author%3Ayomaytk%20OR%20author%3Aaendk%20OR%20author%3Awine99%20OR%20author%3Agaugarg-nv%20OR%20author%3Ataronaeo%20OR%20author%3Aforforever73%20OR%20author%3Achaxu01%20OR%20author%3Alhez%20OR%20author%3AAlekseiNikiforovIBM%20OR%20author%3Anetrunnereve%20OR%20author%3Afairydreaming%20OR%20author%3Aynankani)%20sort%3Aupdated-desc) / [dev stats](https://github.com/ggml-org/llama.cpp-dev) / [lib llama API](https://github.com/ggml-org/llama.cpp/issues/9289) / [llama-server REST API](https://github.com/ggml-org/llama.cpp/issues/9291)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -185,18 +185,28 @@ fi
|
||||
|
||||
## helpers
|
||||
|
||||
# download a file if it does not exist or if it is outdated
|
||||
# download a file unless already cached in $MNT
|
||||
function gg_wget {
|
||||
local out=$1
|
||||
local url=$2
|
||||
|
||||
local cwd=`pwd`
|
||||
local name=`basename $url`
|
||||
local tmp=.gg-wget.$$.tmp
|
||||
|
||||
mkdir -p $out
|
||||
cd $out
|
||||
|
||||
# should not re-download if file is the same
|
||||
wget -nv -c -N $url
|
||||
# cache first: HF artifacts are static, HF sends no Last-Modified for -N
|
||||
if [ ! -s $name ]; then
|
||||
# publish with mv: concurrent runners never read partial files
|
||||
rm -rf $tmp && mkdir $tmp
|
||||
(cd $tmp && wget -nv $url)
|
||||
if [ -s $tmp/$name ]; then
|
||||
mv -f $tmp/$name $name
|
||||
fi
|
||||
rm -rf $tmp
|
||||
fi
|
||||
|
||||
cd $cwd
|
||||
}
|
||||
@@ -254,7 +264,7 @@ function gg_run_ctest_release {
|
||||
(time cmake --build . --config Release -j$(nproc)) 2>&1 | tee -a $OUT/${ci}-make.log
|
||||
|
||||
if [ -z ${GG_BUILD_LOW_PERF} ]; then
|
||||
(time ctest -C Release --output-on-failure -L 'main|python' ${CTEST_EXTRA}) 2>&1 | tee -a $OUT/${ci}-ctest.log
|
||||
(time ctest -C Release --output-on-failure -L main ${CTEST_EXTRA}) 2>&1 | tee -a $OUT/${ci}-ctest.log
|
||||
else
|
||||
(time ctest -C Release --output-on-failure -L main -E test-opt ${CTEST_EXTRA}) 2>&1 | tee -a $OUT/${ci}-ctest.log
|
||||
fi
|
||||
@@ -312,8 +322,8 @@ function gg_run_test_scripts {
|
||||
|
||||
set -e
|
||||
|
||||
(cd ./tools/gguf-split && time bash tests.sh "$SRC/build-ci-release/bin" "$MNT/models") 2>&1 | tee -a $OUT/${ci}-scripts.log
|
||||
(cd ./tools/quantize && time bash tests.sh "$SRC/build-ci-release/bin" "$MNT/models") 2>&1 | tee -a $OUT/${ci}-scripts.log
|
||||
(cd ./tools/gguf-split && time bash tests.sh "$SRC/build-ci-release/bin" "$OUT/scratch") 2>&1 | tee -a $OUT/${ci}-scripts.log
|
||||
(cd ./tools/quantize && time bash tests.sh "$SRC/build-ci-release/bin" "$OUT/scratch") 2>&1 | tee -a $OUT/${ci}-scripts.log
|
||||
|
||||
set +e
|
||||
}
|
||||
@@ -374,10 +384,14 @@ function gg_run_qwen3_0_6b {
|
||||
|
||||
|
||||
gg_wget models-mnt/wikitext/ https://huggingface.co/datasets/ggml-org/ci/resolve/main/wikitext-2-raw-v1.zip
|
||||
unzip -o models-mnt/wikitext/wikitext-2-raw-v1.zip -d models-mnt/wikitext/
|
||||
|
||||
rm -rf $OUT/scratch/qwen3-0.6B $OUT/scratch/wikitext
|
||||
mkdir -p $OUT/scratch/qwen3-0.6B
|
||||
unzip -o models-mnt/wikitext/wikitext-2-raw-v1.zip -d $OUT/scratch/wikitext/
|
||||
|
||||
path_models="../models-mnt/qwen3/0.6B"
|
||||
path_wiki="../models-mnt/wikitext/wikitext-2-raw"
|
||||
path_local="$OUT/scratch/qwen3-0.6B"
|
||||
path_wiki="$OUT/scratch/wikitext/wikitext-2-raw"
|
||||
|
||||
rm -rf build-ci-release && mkdir build-ci-release && cd build-ci-release
|
||||
|
||||
@@ -386,72 +400,15 @@ function gg_run_qwen3_0_6b {
|
||||
(cmake -G "${CMAKE_GENERATOR}" -DCMAKE_BUILD_TYPE=Release ${CMAKE_EXTRA} .. ) 2>&1 | tee -a $OUT/${ci}-cmake.log
|
||||
(time cmake --build . --config Release -j$(nproc)) 2>&1 | tee -a $OUT/${ci}-make.log
|
||||
|
||||
python3 ../convert_hf_to_gguf.py ${path_models} --outfile ${path_models}/ggml-model-f16.gguf --outtype f16
|
||||
python3 ../convert_hf_to_gguf.py ${path_models} --outfile ${path_models}/ggml-model-bf16.gguf --outtype bf16
|
||||
python3 ../convert_hf_to_gguf.py ${path_models} --outfile ${path_local}/ggml-model-f16.gguf --outtype f16
|
||||
python3 ../convert_hf_to_gguf.py ${path_models} --outfile ${path_local}/ggml-model-bf16.gguf --outtype bf16
|
||||
|
||||
model_f16="${path_models}/ggml-model-f16.gguf"
|
||||
model_bf16="${path_models}/ggml-model-bf16.gguf"
|
||||
model_q8_0="${path_models}/ggml-model-q8_0.gguf"
|
||||
model_q4_0="${path_models}/ggml-model-q4_0.gguf"
|
||||
model_q4_1="${path_models}/ggml-model-q4_1.gguf"
|
||||
model_q5_0="${path_models}/ggml-model-q5_0.gguf"
|
||||
model_q5_1="${path_models}/ggml-model-q5_1.gguf"
|
||||
model_q2_k="${path_models}/ggml-model-q2_k.gguf"
|
||||
model_q3_k="${path_models}/ggml-model-q3_k.gguf"
|
||||
model_q4_k="${path_models}/ggml-model-q4_k.gguf"
|
||||
model_q5_k="${path_models}/ggml-model-q5_k.gguf"
|
||||
model_q6_k="${path_models}/ggml-model-q6_k.gguf"
|
||||
model_f16="${path_local}/ggml-model-f16.gguf"
|
||||
model_bf16="${path_local}/ggml-model-bf16.gguf"
|
||||
model_q4_0="${path_local}/ggml-model-q4_0.gguf"
|
||||
|
||||
wiki_test="${path_wiki}/wiki.test.raw"
|
||||
|
||||
./bin/llama-quantize ${model_bf16} ${model_q8_0} q8_0 $(nproc)
|
||||
./bin/llama-quantize ${model_bf16} ${model_q4_0} q4_0 $(nproc)
|
||||
./bin/llama-quantize ${model_bf16} ${model_q4_1} q4_1 $(nproc)
|
||||
./bin/llama-quantize ${model_bf16} ${model_q5_0} q5_0 $(nproc)
|
||||
./bin/llama-quantize ${model_bf16} ${model_q5_1} q5_1 $(nproc)
|
||||
./bin/llama-quantize ${model_bf16} ${model_q2_k} q2_k $(nproc)
|
||||
./bin/llama-quantize ${model_bf16} ${model_q3_k} q3_k $(nproc)
|
||||
./bin/llama-quantize ${model_bf16} ${model_q4_k} q4_k $(nproc)
|
||||
./bin/llama-quantize ${model_bf16} ${model_q5_k} q5_k $(nproc)
|
||||
./bin/llama-quantize ${model_bf16} ${model_q6_k} q6_k $(nproc)
|
||||
|
||||
(time ./bin/llama-fit-params --model ${model_f16} 2>&1 | tee -a $OUT/${ci}-fp-f16.log)
|
||||
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_f16} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-f16.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_bf16} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-bf16.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_q8_0} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-q8_0.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_q4_0} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-q4_0.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_q4_1} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-q4_1.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_q5_0} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-q5_0.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_q5_1} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-q5_1.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_q2_k} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-q2_k.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_q3_k} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-q3_k.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_q4_k} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-q4_k.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_q5_k} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-q5_k.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_q6_k} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-q6_k.log
|
||||
|
||||
(time ./bin/llama-perplexity --model ${model_f16} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-f16.log
|
||||
if [ -z ${GG_BUILD_NO_BF16} ]; then
|
||||
(time ./bin/llama-perplexity --model ${model_bf16} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-bf16.log
|
||||
fi
|
||||
(time ./bin/llama-perplexity --model ${model_q8_0} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-q8_0.log
|
||||
(time ./bin/llama-perplexity --model ${model_q4_0} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-q4_0.log
|
||||
(time ./bin/llama-perplexity --model ${model_q4_1} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-q4_1.log
|
||||
(time ./bin/llama-perplexity --model ${model_q5_0} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-q5_0.log
|
||||
(time ./bin/llama-perplexity --model ${model_q5_1} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-q5_1.log
|
||||
(time ./bin/llama-perplexity --model ${model_q2_k} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-q2_k.log
|
||||
(time ./bin/llama-perplexity --model ${model_q3_k} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-q3_k.log
|
||||
(time ./bin/llama-perplexity --model ${model_q4_k} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-q4_k.log
|
||||
(time ./bin/llama-perplexity --model ${model_q5_k} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-q5_k.log
|
||||
(time ./bin/llama-perplexity --model ${model_q6_k} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-q6_k.log
|
||||
|
||||
(time ./bin/llama-imatrix --model ${model_f16} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-imatrix.log
|
||||
|
||||
(time ./bin/test-llama-context --model ${model_q4_0} -ngl 10 -c 1024 -fa off --no-op-offload) 2>&1 | tee -a $OUT/${ci}-test-llama-context.log
|
||||
(time ./bin/test-llama-context --model ${model_q4_0} -ngl 10 -c 1024 -fa on --no-op-offload) 2>&1 | tee -a $OUT/${ci}-test-llama-context.log
|
||||
(time ./bin/test-llama-context --model ${model_q4_0} -ngl 99 -c 1024 -fa off ) 2>&1 | tee -a $OUT/${ci}-test-llama-context.log
|
||||
(time ./bin/test-llama-context --model ${model_q4_0} -ngl 99 -c 1024 -fa on ) 2>&1 | tee -a $OUT/${ci}-test-llama-context.log
|
||||
|
||||
function check_ppl {
|
||||
qnt="$1"
|
||||
ppl=$(echo "$2" | grep -oE "[0-9]+\.[0-9]+" | tail -n 1)
|
||||
@@ -465,20 +422,56 @@ function gg_run_qwen3_0_6b {
|
||||
return 0
|
||||
}
|
||||
|
||||
for qnt in q8_0 q4_0 q4_1 q5_0 q5_1 q2_k q3_k q4_k q5_k q6_k; do
|
||||
model="${path_local}/ggml-model-${qnt}.gguf"
|
||||
|
||||
./bin/llama-quantize ${model_bf16} ${model} ${qnt} $(nproc)
|
||||
|
||||
(time ./bin/llama-completion -no-cnv --model ${model} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-${qnt}.log
|
||||
(time ./bin/llama-perplexity --model ${model} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-${qnt}.log
|
||||
|
||||
# note: ppl > 20.0 for q2_k with this model
|
||||
if [ "${qnt}" != "q2_k" ]; then
|
||||
check_ppl "${qnt}" "$(cat $OUT/${ci}-tg-${qnt}.log | grep "^\[1\]")"
|
||||
fi
|
||||
|
||||
if [ "${qnt}" = "q4_0" ]; then
|
||||
# q4_0 is kept for other tests
|
||||
q4_0="${path_models}/ggml-model-q4_0.gguf"
|
||||
ln ${model} "$q4_0" 2>/dev/null || { cp ${model} $q4_0.tmp.$$ && mv -f $q4_0.tmp.$$ $q4_0; }
|
||||
else
|
||||
rm -f ${model}
|
||||
fi
|
||||
done
|
||||
|
||||
(time ./bin/llama-fit-params --model ${model_f16} 2>&1 | tee -a $OUT/${ci}-fp-f16.log)
|
||||
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_f16} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-f16.log
|
||||
(time ./bin/llama-completion -no-cnv --model ${model_bf16} -ngl 99 -c 1024 -s 1234 -n 64 --ignore-eos -p "I believe the meaning of life is" ) 2>&1 | tee -a $OUT/${ci}-tg-bf16.log
|
||||
|
||||
(time ./bin/llama-perplexity --model ${model_f16} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-f16.log
|
||||
if [ -z ${GG_BUILD_NO_BF16} ]; then
|
||||
(time ./bin/llama-perplexity --model ${model_bf16} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-tg-bf16.log
|
||||
fi
|
||||
|
||||
(time ./bin/llama-imatrix --model ${model_f16} -f ${wiki_test} -ngl 99 -c 1024 -b 512 --chunks 2 ) 2>&1 | tee -a $OUT/${ci}-imatrix.log
|
||||
|
||||
# TODO: these tests are extremely slow on WebGPU - re-enable when fixed
|
||||
# https://github.com/ggml-org/llama.cpp/actions/runs/39639985732
|
||||
if [ -z ${GG_BUILD_WEBGPU} ]; then
|
||||
(time ./bin/test-llama-context --model ${model_q4_0} -ngl 10 -c 1024 -fa off --no-op-offload) 2>&1 | tee -a $OUT/${ci}-test-llama-context.log
|
||||
(time ./bin/test-llama-context --model ${model_q4_0} -ngl 99 -c 1024 -fa off ) 2>&1 | tee -a $OUT/${ci}-test-llama-context.log
|
||||
fi
|
||||
(time ./bin/test-llama-context --model ${model_q4_0} -ngl 10 -c 1024 -fa on --no-op-offload) 2>&1 | tee -a $OUT/${ci}-test-llama-context.log
|
||||
(time ./bin/test-llama-context --model ${model_q4_0} -ngl 99 -c 1024 -fa on ) 2>&1 | tee -a $OUT/${ci}-test-llama-context.log
|
||||
|
||||
check_ppl "f16" "$(cat $OUT/${ci}-tg-f16.log | grep "^\[1\]")"
|
||||
if [ -z ${GG_BUILD_NO_BF16} ]; then
|
||||
check_ppl "bf16" "$(cat $OUT/${ci}-tg-bf16.log | grep "^\[1\]")"
|
||||
fi
|
||||
check_ppl "q8_0" "$(cat $OUT/${ci}-tg-q8_0.log | grep "^\[1\]")"
|
||||
check_ppl "q4_0" "$(cat $OUT/${ci}-tg-q4_0.log | grep "^\[1\]")"
|
||||
check_ppl "q4_1" "$(cat $OUT/${ci}-tg-q4_1.log | grep "^\[1\]")"
|
||||
check_ppl "q5_0" "$(cat $OUT/${ci}-tg-q5_0.log | grep "^\[1\]")"
|
||||
check_ppl "q5_1" "$(cat $OUT/${ci}-tg-q5_1.log | grep "^\[1\]")"
|
||||
#check_ppl "q2_k" "$(cat $OUT/${ci}-tg-q2_k.log | grep "^\[1\]")" # note: ppl > 20.0 for this quant and model
|
||||
check_ppl "q3_k" "$(cat $OUT/${ci}-tg-q3_k.log | grep "^\[1\]")"
|
||||
check_ppl "q4_k" "$(cat $OUT/${ci}-tg-q4_k.log | grep "^\[1\]")"
|
||||
check_ppl "q5_k" "$(cat $OUT/${ci}-tg-q5_k.log | grep "^\[1\]")"
|
||||
check_ppl "q6_k" "$(cat $OUT/${ci}-tg-q6_k.log | grep "^\[1\]")"
|
||||
|
||||
# scratch is rebuilt on every run; q4_0 is already published to $MNT
|
||||
rm -rf $OUT/scratch/qwen3-0.6B $OUT/scratch/wikitext
|
||||
|
||||
set +e
|
||||
}
|
||||
@@ -501,6 +494,8 @@ function gg_run_embd_bge_small {
|
||||
gg_wget models-mnt/bge-small/1_Pooling https://huggingface.co/BAAI/bge-small-en-v1.5/raw/main/1_Pooling/config.json
|
||||
|
||||
path_models="../models-mnt/bge-small"
|
||||
path_local="$OUT/scratch/bge-small"
|
||||
rm -rf $path_local && mkdir -p $path_local
|
||||
|
||||
rm -rf build-ci-release && mkdir build-ci-release && cd build-ci-release
|
||||
|
||||
@@ -509,10 +504,10 @@ function gg_run_embd_bge_small {
|
||||
(cmake -G "${CMAKE_GENERATOR}" -DCMAKE_BUILD_TYPE=Release ${CMAKE_EXTRA} .. ) 2>&1 | tee -a $OUT/${ci}-cmake.log
|
||||
(time cmake --build . --config Release -j$(nproc)) 2>&1 | tee -a $OUT/${ci}-make.log
|
||||
|
||||
python3 ../convert_hf_to_gguf.py ${path_models} --outfile ${path_models}/ggml-model-f16.gguf
|
||||
python3 ../convert_hf_to_gguf.py ${path_models} --outfile ${path_local}/ggml-model-f16.gguf
|
||||
|
||||
model_f16="${path_models}/ggml-model-f16.gguf"
|
||||
model_q8_0="${path_models}/ggml-model-q8_0.gguf"
|
||||
model_f16="${path_local}/ggml-model-f16.gguf"
|
||||
model_q8_0="${path_local}/ggml-model-q8_0.gguf"
|
||||
|
||||
./bin/llama-quantize ${model_f16} ${model_q8_0} q8_0
|
||||
|
||||
@@ -521,6 +516,8 @@ function gg_run_embd_bge_small {
|
||||
(time ./bin/llama-embedding --model ${model_f16} -p "I believe the meaning of life is" -ngl 99 -c 0 --no-op-offload) 2>&1 | tee -a $OUT/${ci}-tg-f16.log
|
||||
(time ./bin/llama-embedding --model ${model_q8_0} -p "I believe the meaning of life is" -ngl 99 -c 0 --no-op-offload) 2>&1 | tee -a $OUT/${ci}-tg-q8_0.log
|
||||
|
||||
rm -rf $path_local
|
||||
|
||||
set +e
|
||||
}
|
||||
|
||||
@@ -537,6 +534,8 @@ function gg_run_rerank_tiny {
|
||||
gg_wget models-mnt/rerank-tiny/ https://huggingface.co/jinaai/jina-reranker-v1-tiny-en/raw/main/vocab.json
|
||||
|
||||
path_models="../models-mnt/rerank-tiny"
|
||||
path_local="$OUT/scratch/rerank-tiny"
|
||||
rm -rf $path_local && mkdir -p $path_local
|
||||
|
||||
rm -rf build-ci-release && mkdir build-ci-release && cd build-ci-release
|
||||
|
||||
@@ -545,9 +544,9 @@ function gg_run_rerank_tiny {
|
||||
(cmake -G "${CMAKE_GENERATOR}" -DCMAKE_BUILD_TYPE=Release ${CMAKE_EXTRA} .. ) 2>&1 | tee -a $OUT/${ci}-cmake.log
|
||||
(time cmake --build . --config Release -j$(nproc)) 2>&1 | tee -a $OUT/${ci}-make.log
|
||||
|
||||
python3 ../convert_hf_to_gguf.py ${path_models} --outfile ${path_models}/ggml-model-f16.gguf
|
||||
python3 ../convert_hf_to_gguf.py ${path_models} --outfile ${path_local}/ggml-model-f16.gguf
|
||||
|
||||
model_f16="${path_models}/ggml-model-f16.gguf"
|
||||
model_f16="${path_local}/ggml-model-f16.gguf"
|
||||
|
||||
(time ./bin/llama-fit-params --model ${model_f16} 2>&1 | tee -a $OUT/${ci}-fp-f16.log)
|
||||
|
||||
@@ -577,6 +576,8 @@ function gg_run_rerank_tiny {
|
||||
check_score "rerank score 1" "$(cat $OUT/${ci}-rk-f16.log | grep "rerank score 1")" "0.00" "0.05" | tee -a $OUT/${ci}-rk-f16.log
|
||||
check_score "rerank score 2" "$(cat $OUT/${ci}-rk-f16.log | grep "rerank score 2")" "0.10" "0.30" | tee -a $OUT/${ci}-rk-f16.log
|
||||
|
||||
rm -rf $path_local
|
||||
|
||||
set +e
|
||||
}
|
||||
|
||||
@@ -683,11 +684,11 @@ if [ -z ${GG_BUILD_LOW_PERF} ]; then
|
||||
ln -sfn ${mnt_models} ${SRC}/models-mnt
|
||||
|
||||
# Create a fresh python3 venv and enter it
|
||||
if ! python3 -m venv "$MNT/venv"; then
|
||||
echo "Error: Failed to create Python virtual environment at $MNT/venv."
|
||||
if ! python3 -m venv "$OUT/venv"; then
|
||||
echo "Error: Failed to create Python virtual environment at $OUT/venv."
|
||||
exit 1
|
||||
fi
|
||||
source "$MNT/venv/bin/activate"
|
||||
source "$OUT/venv/bin/activate"
|
||||
|
||||
pip install -r ${SRC}/requirements.txt --disable-pip-version-check
|
||||
pip install --editable gguf-py --disable-pip-version-check
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ include("ggml/cmake/common.cmake")
|
||||
|
||||
function(llama_add_compile_flags)
|
||||
if (LLAMA_FATAL_WARNINGS)
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "IntelLLVM")
|
||||
list(APPEND C_FLAGS -Werror)
|
||||
list(APPEND CXX_FLAGS -Werror)
|
||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
set(LLAMA_WIN_APP_MANIFEST "${CMAKE_CURRENT_LIST_DIR}/../common/win-app.manifest")
|
||||
|
||||
if(WIN32)
|
||||
if(MSVC OR CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC")
|
||||
set(LLAMA_WIN_APP_MSVC_ABI TRUE)
|
||||
else()
|
||||
set(LLAMA_WIN_APP_MSVC_ABI FALSE)
|
||||
endif()
|
||||
|
||||
if(NOT LLAMA_WIN_APP_MSVC_ABI)
|
||||
if(CMAKE_VERSION VERSION_LESS 3.21)
|
||||
message(FATAL_ERROR "CMake 3.21 or newer is required to build for Windows with the GNU ABI")
|
||||
endif()
|
||||
enable_language(RC)
|
||||
set(LLAMA_WIN_APP_MANIFEST_RC "${CMAKE_CURRENT_BINARY_DIR}/win-app-manifest.rc")
|
||||
configure_file("${CMAKE_CURRENT_LIST_DIR}/win-app.rc.in" "${LLAMA_WIN_APP_MANIFEST_RC}" @ONLY)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${LLAMA_WIN_APP_MANIFEST}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
function(llama_add_app_manifest target)
|
||||
if(NOT WIN32)
|
||||
return()
|
||||
endif()
|
||||
get_target_property(_aliased ${target} ALIASED_TARGET)
|
||||
if(_aliased)
|
||||
return()
|
||||
endif()
|
||||
get_target_property(_type ${target} TYPE)
|
||||
if(NOT _type STREQUAL "EXECUTABLE")
|
||||
return()
|
||||
endif()
|
||||
if(LLAMA_WIN_APP_MSVC_ABI)
|
||||
target_sources(${target} PRIVATE "${LLAMA_WIN_APP_MANIFEST}")
|
||||
else()
|
||||
if(NOT TARGET llama-win-app-manifest-res)
|
||||
add_library(llama-win-app-manifest-res OBJECT ${LLAMA_WIN_APP_MANIFEST_RC})
|
||||
set_source_files_properties(${LLAMA_WIN_APP_MANIFEST_RC} PROPERTIES OBJECT_DEPENDS "${LLAMA_WIN_APP_MANIFEST}")
|
||||
endif()
|
||||
target_link_libraries(${target} PRIVATE "$<TARGET_OBJECTS:llama-win-app-manifest-res>")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(llama_apply_app_manifests_dir dir)
|
||||
get_property(_targets DIRECTORY "${dir}" PROPERTY BUILDSYSTEM_TARGETS)
|
||||
foreach(_target IN LISTS _targets)
|
||||
llama_add_app_manifest(${_target})
|
||||
endforeach()
|
||||
get_property(_subdirs DIRECTORY "${dir}" PROPERTY SUBDIRECTORIES)
|
||||
foreach(_subdir IN LISTS _subdirs)
|
||||
llama_apply_app_manifests_dir("${_subdir}")
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
function(llama_apply_app_manifests)
|
||||
if(NOT WIN32)
|
||||
return()
|
||||
endif()
|
||||
get_property(_subdirs DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" PROPERTY SUBDIRECTORIES)
|
||||
foreach(_dir IN LISTS _subdirs)
|
||||
get_filename_component(_name "${_dir}" NAME)
|
||||
if(_name STREQUAL "app" OR _name STREQUAL "examples" OR _name STREQUAL "pocs" OR _name STREQUAL "tests" OR _name STREQUAL "tools")
|
||||
llama_apply_app_manifests_dir("${_dir}")
|
||||
endif()
|
||||
endforeach()
|
||||
endfunction()
|
||||
@@ -0,0 +1,3 @@
|
||||
// generated from cmake/win-app.rc.in - do not edit
|
||||
// RT_MANIFEST (24), ID 1: the application manifest shared by all llama.cpp executables
|
||||
1 24 "@LLAMA_WIN_APP_MANIFEST@"
|
||||
@@ -78,6 +78,8 @@ add_library(${TARGET}
|
||||
download.h
|
||||
fit.cpp
|
||||
fit.h
|
||||
file-lock.cpp
|
||||
file-lock.h
|
||||
hf-cache.cpp
|
||||
hf-cache.h
|
||||
http.h
|
||||
@@ -184,3 +186,8 @@ if (LLAMA_LLGUIDANCE)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${TARGET} PUBLIC llama Threads::Threads)
|
||||
|
||||
if (CMAKE_SYSTEM_NAME MATCHES "AIX")
|
||||
# AIX's flock() function comes from libbsd.a
|
||||
target_link_libraries(${TARGET} PRIVATE -lbsd)
|
||||
endif()
|
||||
|
||||
+65
-22
@@ -470,6 +470,14 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
auto opts = handler.opts; // copy
|
||||
opts.callback = callback;
|
||||
|
||||
// another process may hold the cache lock during finalization:
|
||||
// let the download callback cancel that wait too
|
||||
auto finalize = [callback](const hf_cache::hf_file & f) {
|
||||
return hf_cache::finalize_file(f, [callback]() {
|
||||
return !(callback && callback->is_cancelled());
|
||||
});
|
||||
};
|
||||
|
||||
// handle plain "url" if needed
|
||||
auto handle_url = [&](common_params_model & model) {
|
||||
if (!model.url.empty()) {
|
||||
@@ -518,7 +526,7 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
}
|
||||
|
||||
// handle hf_plan tasks
|
||||
auto add_tasks = [&opts, &tasks](const hf_cache::hf_files & model_files,
|
||||
auto add_tasks = [&opts, &tasks, &finalize](const hf_cache::hf_files & model_files,
|
||||
const hf_cache::hf_file & primary,
|
||||
common_params_model & model) {
|
||||
for (size_t i = 0; i < model_files.size(); ++i) {
|
||||
@@ -527,9 +535,9 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
tasks.emplace_back(model_file, opts, [&, is_primary]() {
|
||||
if (is_primary) {
|
||||
// the primary file is the first split (00001-of), use it as model path
|
||||
model.path = hf_cache::finalize_file(model_file);
|
||||
model.path = finalize(model_file);
|
||||
} else {
|
||||
hf_cache::finalize_file(model_file);
|
||||
finalize(model_file);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -581,9 +589,9 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
tasks.emplace_back(plan_spec.mtp, opts, [&]() {
|
||||
// only use the discovered MTP head when no draft path is set yet
|
||||
if (params.speculative.draft.mparams.path.empty()) {
|
||||
params.speculative.draft.mparams.path = hf_cache::finalize_file(plan_spec.mtp);
|
||||
params.speculative.draft.mparams.path = finalize(plan_spec.mtp);
|
||||
} else {
|
||||
hf_cache::finalize_file(plan_spec.mtp);
|
||||
finalize(plan_spec.mtp);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -591,9 +599,9 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
tasks.emplace_back(plan_spec.dflash, opts, [&]() {
|
||||
// only use the discovered DFlash sidecar when no draft path is set yet
|
||||
if (params.speculative.draft.mparams.path.empty()) {
|
||||
params.speculative.draft.mparams.path = hf_cache::finalize_file(plan_spec.dflash);
|
||||
params.speculative.draft.mparams.path = finalize(plan_spec.dflash);
|
||||
} else {
|
||||
hf_cache::finalize_file(plan_spec.dflash);
|
||||
finalize(plan_spec.dflash);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -601,9 +609,9 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
tasks.emplace_back(plan_spec.eagle3, opts, [&]() {
|
||||
// only use the discovered Eagle3 sidecar when no draft path is set yet
|
||||
if (params.speculative.draft.mparams.path.empty()) {
|
||||
params.speculative.draft.mparams.path = hf_cache::finalize_file(plan_spec.eagle3);
|
||||
params.speculative.draft.mparams.path = finalize(plan_spec.eagle3);
|
||||
} else {
|
||||
hf_cache::finalize_file(plan_spec.eagle3);
|
||||
finalize(plan_spec.eagle3);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -611,9 +619,9 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
tasks.emplace_back(plan_spec.dspark, opts, [&]() {
|
||||
// only use the discovered DSpark sidecar when no draft path is set yet
|
||||
if (params.speculative.draft.mparams.path.empty()) {
|
||||
params.speculative.draft.mparams.path = hf_cache::finalize_file(plan_spec.dspark);
|
||||
params.speculative.draft.mparams.path = finalize(plan_spec.dspark);
|
||||
} else {
|
||||
hf_cache::finalize_file(plan_spec.dspark);
|
||||
finalize(plan_spec.dspark);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -634,16 +642,16 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
}
|
||||
if (!plan.mmproj.local_path.empty()) {
|
||||
tasks.emplace_back(plan.mmproj, opts, [&]() {
|
||||
params.mmproj.path = hf_cache::finalize_file(plan.mmproj);
|
||||
params.mmproj.path = finalize(plan.mmproj);
|
||||
});
|
||||
}
|
||||
if (!plan.mtp.local_path.empty() && !had_spec_url) {
|
||||
tasks.emplace_back(plan.mtp, opts, [&]() {
|
||||
// only fall back to the discovered MTP head when no draft was explicitly provided
|
||||
if (params.speculative.draft.mparams.empty()) {
|
||||
params.speculative.draft.mparams.path = hf_cache::finalize_file(plan.mtp);
|
||||
params.speculative.draft.mparams.path = finalize(plan.mtp);
|
||||
} else {
|
||||
hf_cache::finalize_file(plan.mtp);
|
||||
finalize(plan.mtp);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -651,9 +659,9 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
tasks.emplace_back(plan.dflash, opts, [&]() {
|
||||
// only fall back to the discovered DFlash sidecar when no draft was explicitly provided
|
||||
if (params.speculative.draft.mparams.empty()) {
|
||||
params.speculative.draft.mparams.path = hf_cache::finalize_file(plan.dflash);
|
||||
params.speculative.draft.mparams.path = finalize(plan.dflash);
|
||||
} else {
|
||||
hf_cache::finalize_file(plan.dflash);
|
||||
finalize(plan.dflash);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -661,9 +669,9 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
tasks.emplace_back(plan.eagle3, opts, [&]() {
|
||||
// only fall back to the discovered Eagle3 sidecar when no draft was explicitly provided
|
||||
if (params.speculative.draft.mparams.empty()) {
|
||||
params.speculative.draft.mparams.path = hf_cache::finalize_file(plan.eagle3);
|
||||
params.speculative.draft.mparams.path = finalize(plan.eagle3);
|
||||
} else {
|
||||
hf_cache::finalize_file(plan.eagle3);
|
||||
finalize(plan.eagle3);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -671,9 +679,9 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
tasks.emplace_back(plan.dspark, opts, [&]() {
|
||||
// only fall back to the discovered DSpark sidecar when no draft was explicitly provided
|
||||
if (params.speculative.draft.mparams.empty()) {
|
||||
params.speculative.draft.mparams.path = hf_cache::finalize_file(plan.dspark);
|
||||
params.speculative.draft.mparams.path = finalize(plan.dspark);
|
||||
} else {
|
||||
hf_cache::finalize_file(plan.dspark);
|
||||
finalize(plan.dspark);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -681,7 +689,7 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
tasks.emplace_back(plan.preset, opts, [&]() {
|
||||
// if HF repo is a preset repo, we simply run server in router mode with the preset.ini file
|
||||
params.models_preset_hf = params.model.hf_repo; // only for showing a warning
|
||||
params.models_preset = hf_cache::finalize_file(plan.preset);
|
||||
params.models_preset = finalize(plan.preset);
|
||||
// clear the model so the server starts in router mode
|
||||
params.model.path.clear();
|
||||
params.model.hf_repo.clear();
|
||||
@@ -916,7 +924,7 @@ static bool common_params_parse_ex(int argc, char ** argv, common_params_context
|
||||
// TODO @ngxson : maybe show a list of available models in CLI in this case
|
||||
bool can_skip_model = params.usage || params.completion || !params.server_base.empty();
|
||||
if (!can_skip_model && params.model.path.empty()) {
|
||||
throw std::invalid_argument("error: --model is required\n");
|
||||
throw std::invalid_argument("error: --model or -hf is required\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3494,6 +3502,26 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
|
||||
params.ui = value;
|
||||
}
|
||||
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_UI"));
|
||||
add_opt(common_arg(
|
||||
{"--connect"},
|
||||
string_format("(experimental) open a peer-to-peer tunnel for server using llama-connect (default: %s)", params.server_connect ? "enabled" : "disabled"),
|
||||
[](common_params & params) {
|
||||
params.server_connect = true;
|
||||
}
|
||||
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_CONNECT"));
|
||||
add_opt(common_arg(
|
||||
{"--connect-code"}, "CODE",
|
||||
"manually specify a 40-character code for --connect (default: generate a new one for each run)",
|
||||
[](common_params & params, const std::string & value) {
|
||||
std::string val = value;
|
||||
string_replace_all(val, "-", "");
|
||||
string_replace_all(val, " ", "");
|
||||
if (val.size() != 40) {
|
||||
throw std::invalid_argument(string_format("error: invalid connect code '%s', must be 40 characters\n", value.c_str()));
|
||||
}
|
||||
params.server_connect_code = val;
|
||||
}
|
||||
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_CONNECT_CODE"));
|
||||
add_opt(common_arg(
|
||||
{"--embedding", "--embeddings"},
|
||||
string_format("restrict to only support embedding use case; use only with dedicated embedding models (default: %s)", params.embedding ? "enabled" : "disabled"),
|
||||
@@ -3680,6 +3708,13 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
|
||||
params.models_max = value;
|
||||
}
|
||||
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_MODELS_MAX"));
|
||||
add_opt(common_arg(
|
||||
{"--models-memory-margin"}, "N",
|
||||
string_format("for router server, MiB of device memory to keep free per device, unload LRU models until a new one fits (default: %d, 0 = disabled)", params.models_memory_margin),
|
||||
[](common_params & params, int value) {
|
||||
params.models_memory_margin = value;
|
||||
}
|
||||
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_MODELS_MEMORY_MARGIN"));
|
||||
add_opt(common_arg(
|
||||
{"--models-autoload"},
|
||||
{"--no-models-autoload"},
|
||||
@@ -4477,6 +4512,14 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
|
||||
params.tts_lang = value;
|
||||
}
|
||||
).set_examples({LLAMA_EXAMPLE_TTS}));
|
||||
add_opt(common_arg(
|
||||
{"--tts-voice"}, "VOICE",
|
||||
"voice for audio generation (model-dependent)\n"
|
||||
"see tts/README.md for per-model usage notes",
|
||||
[](common_params & params, const std::string & value) {
|
||||
params.tts_voice = value;
|
||||
}
|
||||
).set_examples({LLAMA_EXAMPLE_TTS}));
|
||||
add_opt(common_arg(
|
||||
{"--tts-speaker-file"}, "FNAME",
|
||||
"speaker file path for audio generation",
|
||||
|
||||
+215
-29
@@ -190,10 +190,17 @@ json common_chat_msg::to_json_oaicompat(bool concat_typed_text) const {
|
||||
} else {
|
||||
auto & parts = jmsg["content"] = json::array();
|
||||
for (const auto & part : content_parts) {
|
||||
parts.push_back({
|
||||
{"type", part.type},
|
||||
{"text", part.text},
|
||||
});
|
||||
if (part.type == "tool_reference") {
|
||||
parts.push_back({
|
||||
{"type", part.type},
|
||||
{"name", part.name},
|
||||
});
|
||||
} else {
|
||||
parts.push_back({
|
||||
{"type", part.type},
|
||||
{"text", part.text},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -309,6 +316,7 @@ struct common_chat_templates {
|
||||
bool has_explicit_template; // Model had builtin template or template overridden was specified.
|
||||
std::unique_ptr<common_chat_template> template_default; // always set (defaults to chatml)
|
||||
std::unique_ptr<common_chat_template> template_tool_use;
|
||||
std::unique_ptr<common_chat_template> template_tts; // the prompt of a text to speak
|
||||
};
|
||||
|
||||
common_chat_tool_choice common_chat_tool_choice_parse_oaicompat(const std::string & tool_choice) {
|
||||
@@ -370,12 +378,16 @@ std::vector<common_chat_msg> common_chat_msgs_parse_oaicompat(const json & messa
|
||||
throw std::invalid_argument("Missing content part type: " + part.dump());
|
||||
}
|
||||
const auto & type = part.at("type");
|
||||
if (type != "text" && type != "media_marker") {
|
||||
throw std::invalid_argument("Unsupported content part type: " + type.dump());
|
||||
}
|
||||
common_chat_msg_content_part msg_part;
|
||||
msg_part.type = type;
|
||||
msg_part.text = part.at("text");
|
||||
if (type == "text" || type == "media_marker") {
|
||||
msg_part.text = part.at("text");
|
||||
} else if (type == "tool_reference") {
|
||||
// names a request tool, the template renders its definition
|
||||
msg_part.name = part.at("name");
|
||||
} else {
|
||||
throw std::invalid_argument("Unsupported content part type: " + type.dump());
|
||||
}
|
||||
msg.content_parts.push_back(msg_part);
|
||||
}
|
||||
} else if (!content.is_null()) {
|
||||
@@ -428,6 +440,41 @@ std::vector<common_chat_msg> common_chat_msgs_parse_oaicompat(const json & messa
|
||||
if (message.contains("tool_call_id")) {
|
||||
msg.tool_call_id = message.at("tool_call_id");
|
||||
}
|
||||
{
|
||||
json extra = json::object();
|
||||
for (const auto & [k, v] : message.items()) {
|
||||
if (k != "role" && k != "content" && k != "tool_calls" && k != "reasoning_content" &&
|
||||
k != "name" && k != "tool_call_id") {
|
||||
extra[k] = v;
|
||||
}
|
||||
}
|
||||
if (has_tool_calls) {
|
||||
// per tool call: keys besides type/function/id, and function keys besides name/arguments
|
||||
json tc_extra = json::array();
|
||||
bool any = false;
|
||||
for (const auto & tool_call : message.at("tool_calls")) {
|
||||
json e = json::object();
|
||||
for (const auto & [k, v] : tool_call.items()) {
|
||||
if (k != "type" && k != "function" && k != "id") {
|
||||
e[k] = v;
|
||||
}
|
||||
}
|
||||
for (const auto & [k, v] : tool_call.at("function").items()) {
|
||||
if (k != "name" && k != "arguments") {
|
||||
e["function"][k] = v;
|
||||
}
|
||||
}
|
||||
any = any || !e.empty();
|
||||
tc_extra.push_back(e);
|
||||
}
|
||||
if (any) {
|
||||
extra["__tool_call_extras"] = tc_extra;
|
||||
}
|
||||
}
|
||||
if (!extra.empty()) {
|
||||
msg.extra_json = extra.dump();
|
||||
}
|
||||
}
|
||||
|
||||
msgs.push_back(msg);
|
||||
}
|
||||
@@ -516,6 +563,50 @@ static json render_message_to_json(const std::vector<common_chat_msg> & msgs, co
|
||||
return messages_inp_normalizer(c).normalize(messages);
|
||||
}
|
||||
|
||||
// DeepSeek V4.1 reads non-OpenAI message keys and joins content blocks with "\n\n" around images,
|
||||
// so keep media markers as separate text parts and restore the extra keys
|
||||
static json render_message_to_json_deepseek_v4_1(const std::vector<common_chat_msg> & msgs, const jinja::caps & c) {
|
||||
json messages = json::array();
|
||||
for (const auto & msg : msgs) {
|
||||
json jmsg;
|
||||
if (msg.contains_media() && msg.content.empty()) {
|
||||
common_chat_msg typed = msg;
|
||||
for (auto & part : typed.content_parts) {
|
||||
part.type = "text";
|
||||
}
|
||||
jmsg = typed.to_json_oaicompat(/* concat_typed_text= */ false);
|
||||
} else {
|
||||
jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ false);
|
||||
}
|
||||
if (!msg.extra_json.empty()) {
|
||||
const json extra = json::parse(msg.extra_json);
|
||||
for (const auto & [k, v] : extra.items()) {
|
||||
if (k == "__tool_call_extras") {
|
||||
if (!jmsg.contains("tool_calls")) {
|
||||
continue;
|
||||
}
|
||||
auto & tcs = jmsg.at("tool_calls");
|
||||
for (size_t i = 0; i < tcs.size() && i < v.size(); i++) {
|
||||
for (const auto & [tk, tv] : v[i].items()) {
|
||||
if (tk == "function") {
|
||||
for (const auto & [fk, fv] : tv.items()) {
|
||||
tcs[i]["function"][fk] = fv;
|
||||
}
|
||||
} else {
|
||||
tcs[i][tk] = tv;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!jmsg.contains(k)) {
|
||||
jmsg[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
messages.push_back(std::move(jmsg));
|
||||
}
|
||||
return messages_inp_normalizer(c).normalize(messages);
|
||||
}
|
||||
|
||||
// DEPRECATED: only used in tests
|
||||
json common_chat_msgs_to_json_oaicompat(const std::vector<common_chat_msg> & msgs, bool concat_typed_text) {
|
||||
jinja::caps c;
|
||||
@@ -531,13 +622,17 @@ json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_tool> & t
|
||||
|
||||
auto result = json::array();
|
||||
for (const auto & tool : tools) {
|
||||
json function {
|
||||
{ "name", tool.name },
|
||||
{ "description", tool.description },
|
||||
{ "parameters", json::parse(tool.parameters) },
|
||||
};
|
||||
if (tool.defer_loading) {
|
||||
function["defer_loading"] = true;
|
||||
}
|
||||
result.push_back({
|
||||
{ "type", "function" },
|
||||
{ "function", {
|
||||
{ "name", tool.name },
|
||||
{ "description", tool.description },
|
||||
{ "parameters", json::parse(tool.parameters) },
|
||||
}},
|
||||
{ "function", function },
|
||||
});
|
||||
}
|
||||
return result;
|
||||
@@ -574,10 +669,19 @@ std::vector<common_chat_tool> common_chat_tools_parse_oaicompat(const json & too
|
||||
}
|
||||
|
||||
const auto & function = tool.at("function");
|
||||
|
||||
// "defer_loading" sits on the function, or on the tool next to "type"; the function wins
|
||||
const json & defer = function.contains("defer_loading") ? function.at("defer_loading")
|
||||
: tool.value("defer_loading", json(false));
|
||||
if (!defer.is_boolean()) {
|
||||
throw std::invalid_argument("Expected 'defer_loading' to be a boolean, got " + defer.dump());
|
||||
}
|
||||
|
||||
result.push_back({
|
||||
/* .name = */ function.at("name"),
|
||||
/* .description = */ function.value("description", ""),
|
||||
/* .parameters = */ function.value("parameters", json::object()).dump(),
|
||||
/* .defer_loading = */ defer.get<bool>(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -700,19 +804,70 @@ bool common_chat_templates_was_explicit(const struct common_chat_templates * tmp
|
||||
return tmpls->has_explicit_template;
|
||||
}
|
||||
|
||||
common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates) {
|
||||
// renders a task template (e.g. "tts") with the given variables
|
||||
static std::string common_chat_template_render(const common_chat_template & tmpl, const json & vars) {
|
||||
jinja::context ctx(tmpl.source());
|
||||
jinja::global_from_json(ctx, vars, false);
|
||||
ctx.set_val("bos_token", jinja::mk_val<jinja::value_string>(tmpl.bos_token()));
|
||||
ctx.set_val("eos_token", jinja::mk_val<jinja::value_string>(tmpl.eos_token()));
|
||||
jinja::runtime runtime(ctx);
|
||||
return jinja::runtime::gather_string_parts(runtime.execute(tmpl.prog))->as_string().str();
|
||||
}
|
||||
|
||||
// LFM3 format detection: template renders deferred tools inside <additional_tools>...</additional_tools>
|
||||
static bool is_lfm3_template(const std::string & src) {
|
||||
return src.find("<additional_tools>") != std::string::npos;
|
||||
}
|
||||
|
||||
// LFM3-Audio: a known language code maps to its name, no language lets the model detect it
|
||||
static std::string lfm3_asr_system_prompt(const std::string & language) {
|
||||
static const std::map<std::string, std::string> names = {
|
||||
{ "ar", "Arabic" }, { "da", "Danish" }, { "de", "German" }, { "el", "Greek" }, { "en", "English" },
|
||||
{ "es", "Spanish" }, { "fi", "Finnish" }, { "fr", "French" }, { "he", "Hebrew" }, { "it", "Italian" },
|
||||
{ "ja", "Japanese" }, { "ko", "Korean" }, { "nl", "Dutch" }, { "pl", "Polish" }, { "pt", "Portuguese" },
|
||||
{ "sv", "Swedish" }, { "tr", "Turkish" }, { "zh", "Chinese" },
|
||||
};
|
||||
if (language.empty()) {
|
||||
return "Perform ASR.";
|
||||
}
|
||||
std::string code = language;
|
||||
std::transform(code.begin(), code.end(), code.begin(), [](unsigned char c) { return (char) std::tolower(c); });
|
||||
const auto it = names.find(code);
|
||||
return "Perform ASR in " + (it != names.end() ? it->second : language) + ".";
|
||||
}
|
||||
|
||||
common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates, const std::string & language) {
|
||||
common_chat_prompt_preset asr_preset;
|
||||
asr_preset.system = "";
|
||||
asr_preset.user = "Transcribe audio to text";
|
||||
|
||||
if (chat_templates && chat_templates->template_default && is_lfm2_template(chat_templates->template_default->source())) {
|
||||
asr_preset.system = "Perform ASR.";
|
||||
asr_preset.user = "";
|
||||
if (chat_templates && chat_templates->template_default) {
|
||||
const std::string src = chat_templates->template_default->source();
|
||||
if (is_lfm2_template(src)) {
|
||||
asr_preset.system = "Perform ASR.";
|
||||
asr_preset.user = "";
|
||||
} else if (is_lfm3_template(src)) {
|
||||
asr_preset.system = lfm3_asr_system_prompt(language);
|
||||
asr_preset.user = "";
|
||||
asr_preset.has_language = true;
|
||||
}
|
||||
}
|
||||
|
||||
return asr_preset;
|
||||
}
|
||||
|
||||
std::string common_chat_get_tts_prompt(const common_chat_templates * chat_templates, const std::string & text,
|
||||
const std::string & language, const std::string & voice) {
|
||||
if (!chat_templates || !chat_templates->template_tts) {
|
||||
return "";
|
||||
}
|
||||
return common_chat_template_render(*chat_templates->template_tts, {
|
||||
{"messages", json::array({{{"role", "user"}, {"content", text}}})},
|
||||
{"language", language},
|
||||
{"voice", voice},
|
||||
});
|
||||
}
|
||||
|
||||
std::string common_chat_templates_source(const struct common_chat_templates * tmpls, const std::string & variant) {
|
||||
if (!variant.empty()) {
|
||||
if (variant == "tool_use") {
|
||||
@@ -823,6 +978,16 @@ common_chat_templates_ptr common_chat_templates_init(const struct llama_model *
|
||||
LOG_ERR("%s: failed to parse tool use chat template (ignoring it): %s\n", __func__, e.what());
|
||||
}
|
||||
}
|
||||
// task templates of the model, kept with a chat template override
|
||||
if (model) {
|
||||
if (const char * src = llama_model_chat_template(model, "tts")) {
|
||||
try {
|
||||
tmpls->template_tts = std::make_unique<common_chat_template>(src, token_bos, token_eos);
|
||||
} catch (const std::exception & e) {
|
||||
LOG_ERR("%s: failed to parse the tts chat template (ignoring it): %s\n", __func__, e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmpls;
|
||||
}
|
||||
|
||||
@@ -1104,12 +1269,25 @@ static common_chat_params_init_fn common_chat_template_detect_params_init(const
|
||||
return common_chat_params_init_llm_jp_harmony;
|
||||
}
|
||||
|
||||
// Harmony XML tool calls, checked before the GPT-OSS JSON calls
|
||||
if (src.find("<|channel|>") != std::string::npos && src.find("<|constrain|>xml") != std::string::npos &&
|
||||
src.find("rfl:parameter") != std::string::npos) {
|
||||
LOG_DBG("Using specialized template: Harmony XML\n");
|
||||
return common_chat_params_init_harmony_xml;
|
||||
}
|
||||
|
||||
// GPT-OSS - has unique channel-based structure that needs dedicated handler
|
||||
if (src.find("<|channel|>") != std::string::npos) {
|
||||
LOG_DBG("Using specialized template: GPT-OSS\n");
|
||||
return common_chat_params_init_gpt_oss;
|
||||
}
|
||||
|
||||
// Muse Spark shares the Muse recipient protocol and adds a commentary recipient.
|
||||
if (src.find("to=commentary") != std::string::npos && src.find("<|eom|>") != std::string::npos) {
|
||||
LOG_DBG("Using specialized template: Muse Spark\n");
|
||||
return common_chat_params_init_muse_spark;
|
||||
}
|
||||
|
||||
// Muse Glimmer format using " to=<recipient>" recipients and <|eom|>/<|eot|> message terminators.
|
||||
if (src.find("<atem:function_calls>") != std::string::npos && src.find("<|eom|>") != std::string::npos) {
|
||||
LOG_DBG("Using specialized template: Muse Glimmer\n");
|
||||
@@ -1195,11 +1373,13 @@ static common_chat_params_init_fn common_chat_template_detect_params_init(const
|
||||
// DeepSeek V3.2/V4 format detection: template defines dsml_token and uses it for tool calls.
|
||||
// The template source contains the token as a variable assignment, not as a literal in markup.
|
||||
// V3.2 names the tool call block "function_calls", V4 names it "tool_calls".
|
||||
if (src.find("dsml_token") != std::string::npos &&
|
||||
src.find("DSML") != std::string::npos &&
|
||||
(src.find("function_calls") != std::string::npos ||
|
||||
src.find("tool_calls") != std::string::npos)) {
|
||||
LOG_DBG("Using specialized template: DeepSeek V3.2/V4\n");
|
||||
// V4.1 tag names have a leading space and its template does not name the dsml_token variable.
|
||||
if ((src.find("dsml_token") != std::string::npos &&
|
||||
src.find("DSML") != std::string::npos &&
|
||||
(src.find("function_calls") != std::string::npos ||
|
||||
src.find("tool_calls") != std::string::npos)) ||
|
||||
src.find("|DSML| calls>") != std::string::npos) {
|
||||
LOG_DBG("Using specialized template: DeepSeek V3.2/V4/V4.1\n");
|
||||
return common_chat_params_init_deepseek_v3_2;
|
||||
}
|
||||
|
||||
@@ -1300,7 +1480,9 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_
|
||||
workaround::trim_all_content(trimmed_messages);
|
||||
messages_to_render = &trimmed_messages;
|
||||
}
|
||||
params.messages = render_message_to_json(*messages_to_render, tmpl.original_caps());
|
||||
params.messages = src.find("|DSML| calls>") != std::string::npos ?
|
||||
render_message_to_json_deepseek_v4_1(*messages_to_render, tmpl.original_caps()) :
|
||||
render_message_to_json(*messages_to_render, tmpl.original_caps());
|
||||
params.tool_choice = inputs.tool_choice;
|
||||
params.reasoning_format = inputs.reasoning_format;
|
||||
params.enable_thinking = inputs.enable_thinking;
|
||||
@@ -1621,6 +1803,9 @@ common_chat_msg common_chat_peg_parse(const common_peg_arena & src_pars
|
||||
//LOG_DBG("Parsing PEG input with format %s: %s\n", common_chat_format_name(params.format), effective_input.text.c_str());
|
||||
|
||||
common_peg_parse_flags flags = COMMON_PEG_PARSE_FLAG_LENIENT;
|
||||
if (!is_partial) {
|
||||
flags |= COMMON_PEG_PARSE_FLAG_COMPLETE;
|
||||
}
|
||||
if (params.debug) {
|
||||
flags |= COMMON_PEG_PARSE_FLAG_DEBUG;
|
||||
}
|
||||
@@ -1758,12 +1943,13 @@ const common_chat_msg & common_chat_session::finish(const common_chat_input & ch
|
||||
return result;
|
||||
}
|
||||
|
||||
std::map<std::string, bool> common_chat_templates_get_caps(const common_chat_templates * chat_templates) {
|
||||
common_json common_chat_templates_get_caps(const common_chat_templates * chat_templates) {
|
||||
GGML_ASSERT(chat_templates != nullptr);
|
||||
GGML_ASSERT(chat_templates->template_default != nullptr);
|
||||
if (chat_templates->template_tool_use != nullptr) {
|
||||
// take the more expressive template when available
|
||||
return chat_templates->template_tool_use->caps.to_map();
|
||||
}
|
||||
return chat_templates->template_default->caps.to_map();
|
||||
// take the more expressive template when available
|
||||
const auto & caps = chat_templates->template_tool_use != nullptr ? chat_templates->template_tool_use->caps
|
||||
: chat_templates->template_default->caps;
|
||||
common_json result = caps.to_map();
|
||||
result["reasoning_effort_levels"] = caps.reasoning_effort_levels;
|
||||
return result;
|
||||
}
|
||||
|
||||
+15
-3
@@ -45,6 +45,7 @@ struct common_chat_tool_call {
|
||||
struct common_chat_msg_content_part {
|
||||
std::string type;
|
||||
std::string text;
|
||||
std::string name; // tool_reference: name of a tool from the request tools
|
||||
|
||||
// TODO @ngxson : no known chat templates support reasoning_content in content parts yet
|
||||
// this can be useful for models with interleaved thinking (like Kimi-K2)
|
||||
@@ -52,7 +53,7 @@ struct common_chat_msg_content_part {
|
||||
// std::string reasoning_content;
|
||||
|
||||
bool operator==(const common_chat_msg_content_part & other) const {
|
||||
return type == other.type && text == other.text;
|
||||
return type == other.type && text == other.text && name == other.name;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -94,6 +95,8 @@ struct common_chat_msg {
|
||||
std::string reasoning_content;
|
||||
std::string tool_name;
|
||||
std::string tool_call_id;
|
||||
// non-OpenAI message keys as a JSON object string, only for templates that read them (DeepSeek V4.1)
|
||||
std::string extra_json;
|
||||
|
||||
common_json to_json_oaicompat(bool concat_typed_text = false) const;
|
||||
|
||||
@@ -224,6 +227,9 @@ struct common_chat_tool {
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string parameters;
|
||||
|
||||
// withheld from the system prompt, a tool_reference reveals it later in the conversation
|
||||
bool defer_loading = false;
|
||||
};
|
||||
|
||||
enum common_chat_tool_choice {
|
||||
@@ -466,7 +472,7 @@ common_json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_to
|
||||
common_json common_chat_tool_parameters(const common_json & function);
|
||||
|
||||
// get template caps, useful for reporting to server /props endpoint
|
||||
std::map<std::string, bool> common_chat_templates_get_caps(const common_chat_templates * chat_templates);
|
||||
common_json common_chat_templates_get_caps(const common_chat_templates * chat_templates);
|
||||
|
||||
common_chat_prompt common_chat_template_direct_apply(
|
||||
const common_chat_template & tmpl,
|
||||
@@ -485,6 +491,12 @@ std::optional<common_chat_params> common_chat_try_specialized_template(
|
||||
struct common_chat_prompt_preset {
|
||||
std::string system;
|
||||
std::string user;
|
||||
bool has_language = false; // the preset already names the requested language
|
||||
};
|
||||
|
||||
common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates);
|
||||
common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates, const std::string & language = "");
|
||||
|
||||
// prompt of the model's "tts" chat template for the text to speak, empty if the model has none
|
||||
// language and voice are optional, the template defines their defaults
|
||||
std::string common_chat_get_tts_prompt(const common_chat_templates * chat_templates, const std::string & text,
|
||||
const std::string & language = "", const std::string & voice = "");
|
||||
|
||||
+132
-82
@@ -26,6 +26,7 @@
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <random>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -218,6 +219,23 @@ int32_t common_cpu_get_num_math() {
|
||||
}
|
||||
}
|
||||
}
|
||||
#elif defined(__riscv) && defined(__linux__)
|
||||
std::ifstream cpuinfo("/proc/cpuinfo");
|
||||
std::string line;
|
||||
int32_t n_a100 = 0;
|
||||
while (std::getline(cpuinfo, line)) {
|
||||
std::istringstream fields(line);
|
||||
std::string key;
|
||||
std::string separator;
|
||||
uint64_t marchid;
|
||||
if (fields >> key >> separator >> std::hex >> marchid &&
|
||||
key == "marchid" && separator == ":" && marchid == UINT64_C(0x8000000041000002)) {
|
||||
++n_a100;
|
||||
}
|
||||
}
|
||||
if (n_a100 > 0) {
|
||||
return n_a100;
|
||||
}
|
||||
#elif defined(__powerpc64__) || defined(__powerpc__)
|
||||
int32_t smt_factor = 1;
|
||||
int phy_cpus = common_cpu_get_num_physical_cores();
|
||||
@@ -741,50 +759,56 @@ static inline bool glob_class_match(const char c, const char * pattern, const ch
|
||||
}
|
||||
|
||||
// simple glob: * matches non-/ chars, ** matches anything including /, [] matches character class
|
||||
static inline bool glob_match(const char * pattern, const char * str) {
|
||||
if (*pattern == '\0') {
|
||||
return *str == '\0';
|
||||
}
|
||||
if (pattern[0] == '*' && pattern[1] == '*') {
|
||||
const char * p = pattern + 2;
|
||||
if (glob_match(p, str)) return true;
|
||||
if (*str != '\0') return glob_match(pattern, str + 1);
|
||||
// only wildcards recurse, too many wildcards: no match
|
||||
static bool glob_match(const char * pattern, const char * str, int depth = 0) {
|
||||
if (depth >= COMMON_RECURSIVE_LIMIT) {
|
||||
return false;
|
||||
}
|
||||
if (*pattern == '*') {
|
||||
const char * p = pattern + 1;
|
||||
for (; *str != '\0' && *str != '/'; str++) {
|
||||
if (glob_match(p, str)) return true;
|
||||
}
|
||||
return glob_match(p, str);
|
||||
}
|
||||
if (*pattern == '?' && *str != '\0' && *str != '/') {
|
||||
return glob_match(pattern + 1, str + 1);
|
||||
}
|
||||
if (*pattern == '[') {
|
||||
const char * class_end = pattern + 1;
|
||||
// If first character after '[' is ']' or '-', treat it as literal
|
||||
if (*class_end == ']' || *class_end == '-') {
|
||||
class_end++;
|
||||
}
|
||||
while (*class_end != '\0' && *class_end != ']') {
|
||||
class_end++;
|
||||
}
|
||||
if (*class_end == ']') {
|
||||
if (*str == '\0') return false;
|
||||
bool matched = glob_class_match(*str, pattern + 1, class_end);
|
||||
return matched && glob_match(class_end + 1, str + 1);
|
||||
} else {
|
||||
if (*str == '[') {
|
||||
return glob_match(pattern + 1, str + 1);
|
||||
while (*pattern != '\0') {
|
||||
if (pattern[0] == '*' && pattern[1] == '*') {
|
||||
const char * p = pattern + 2;
|
||||
for (;; str++) {
|
||||
if (glob_match(p, str, depth + 1)) return true;
|
||||
if (*str == '\0') return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (*pattern == '*') {
|
||||
const char * p = pattern + 1;
|
||||
for (; *str != '\0' && *str != '/'; str++) {
|
||||
if (glob_match(p, str, depth + 1)) return true;
|
||||
}
|
||||
return glob_match(p, str, depth + 1);
|
||||
}
|
||||
if (*pattern == '?' && *str != '\0' && *str != '/') {
|
||||
pattern++;
|
||||
str++;
|
||||
continue;
|
||||
}
|
||||
if (*pattern == '[') {
|
||||
const char * class_end = pattern + 1;
|
||||
// If first character after '[' is ']' or '-', treat it as literal
|
||||
if (*class_end == ']' || *class_end == '-') {
|
||||
class_end++;
|
||||
}
|
||||
while (*class_end != '\0' && *class_end != ']') {
|
||||
class_end++;
|
||||
}
|
||||
if (*class_end == ']') {
|
||||
if (*str == '\0' || !glob_class_match(*str, pattern + 1, class_end)) return false;
|
||||
pattern = class_end + 1;
|
||||
str++;
|
||||
continue;
|
||||
}
|
||||
if (*str != '[') return false;
|
||||
pattern++;
|
||||
str++;
|
||||
continue;
|
||||
}
|
||||
if (*pattern != *str) return false;
|
||||
pattern++;
|
||||
str++;
|
||||
}
|
||||
if (*pattern == *str) {
|
||||
return glob_match(pattern + 1, str + 1);
|
||||
}
|
||||
return false;
|
||||
return *str == '\0';
|
||||
}
|
||||
|
||||
bool glob_match(const std::string & pattern, const std::string & str) {
|
||||
@@ -917,9 +941,13 @@ std::string fs_path_to_utf8(const std::filesystem::path & path) {
|
||||
}
|
||||
|
||||
void fs_write_atomic(const std::filesystem::path & path, const std::string & data) {
|
||||
// a random suffix gives every writer its own temporary file, across processes and containers
|
||||
std::random_device rd;
|
||||
const uint64_t id = ((uint64_t) rd() << 32) | rd();
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::path path_tmp = path;
|
||||
path_tmp += ".tmp";
|
||||
path_tmp += "." + std::to_string(id) + ".tmp";
|
||||
|
||||
if (path.has_parent_path()) {
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
@@ -1174,6 +1202,11 @@ static const std::map<common_decision_type, std::string> COMMON_DECISION_TYPE_NA
|
||||
{ COMMON_DECISION_TYPE_VLLM_D3, "vllm-d3" },
|
||||
{ COMMON_DECISION_TYPE_LFM2_D1, "lfm2-d1" },
|
||||
{ COMMON_DECISION_TYPE_LFM2_D1_OMNI, "lfm2-d1-omni" },
|
||||
{ COMMON_DECISION_TYPE_GRANITE_SWITCH, "granite-switch"},
|
||||
{ COMMON_DECISION_TYPE_H2O_LIGHTNING, "h2o-lightning" },
|
||||
{ COMMON_DECISION_TYPE_MINICPM_OCR_JEV, "openbmb-minicpm-ocr-jev" },
|
||||
{ COMMON_DECISION_TYPE_LFM3_D1, "lfm3-d1" },
|
||||
{ COMMON_DECISION_TYPE_LFM3_D1_OMNI, "lfm3-d1-omni" },
|
||||
// for future models, ALWAYS add org name prefix to avoid name collisions, e.g. "orgname-modelname"
|
||||
};
|
||||
|
||||
@@ -1233,47 +1266,56 @@ common_gguf_info common_get_gguf_info(const std::string & fname) {
|
||||
info.n_ctx_train = gguf_get_val_u32(gguf_ctx.get(), ctx_id);
|
||||
}
|
||||
|
||||
// same key as llama_model_chat_template(model, nullptr)
|
||||
info.has_chat_tmpl = gguf_find_key(gguf_ctx.get(), "tokenizer.chat_template") >= 0;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
void common_fit_apply(common_params & params, llama_model_params & mparams, llama_context_params & cparams) {
|
||||
if (!params.fit_params) {
|
||||
return;
|
||||
}
|
||||
|
||||
COM_TRC("%s", "fitting params to device memory ...\n");
|
||||
COM_TRC("%s", "(for bugs during this step try to reproduce them with -fit off, or provide --verbose logs if the bug only occurs with -fit on)\n");
|
||||
|
||||
// the draft context is created from the same base params and follows the main context, fit both together
|
||||
const bool has_draft = params.speculative.has_dft();
|
||||
const bool spec_mtp = std::find(params.speculative.types.begin(), params.speculative.types.end(),
|
||||
COMMON_SPECULATIVE_TYPE_DRAFT_MTP) != params.speculative.types.end();
|
||||
|
||||
common_params params_dft = common_base_params_to_speculative(params);
|
||||
|
||||
auto mparams_dft = common_model_params_to_llama(params_dft);
|
||||
auto cparams_dft = common_context_params_to_llama(params_dft);
|
||||
if (spec_mtp) {
|
||||
cparams_dft.ctx_type = LLAMA_CONTEXT_TYPE_MTP;
|
||||
}
|
||||
cparams_dft.n_rs_seq = 0;
|
||||
|
||||
const common_fit_extra_model extra = {
|
||||
/*.path_model =*/ params_dft.model.path.c_str(),
|
||||
/*.mparams =*/ &mparams_dft,
|
||||
/*.cparams =*/ &cparams_dft,
|
||||
/*.shares_model =*/ !has_draft, // an MTP context runs on the weights of the main model
|
||||
};
|
||||
|
||||
common_fit_params(params.model.path.c_str(), &mparams, &cparams,
|
||||
params.tensor_split,
|
||||
params.tensor_buft_overrides.data(),
|
||||
params.fit_params_target.data(),
|
||||
params.fit_params_min_ctx,
|
||||
has_draft || spec_mtp ? &extra : nullptr,
|
||||
params.verbosity >= LOG_LEVEL_DEBUG ? GGML_LOG_LEVEL_DEBUG : GGML_LOG_LEVEL_ERROR);
|
||||
}
|
||||
|
||||
common_init_result::common_init_result(common_params & params, bool model_only) :
|
||||
pimpl(new impl{}) {
|
||||
auto mparams = common_model_params_to_llama(params);
|
||||
auto cparams = common_context_params_to_llama(params);
|
||||
|
||||
if (params.fit_params) {
|
||||
COM_TRC("%s", "fitting params to device memory ...\n");
|
||||
COM_TRC("%s", "(for bugs during this step try to reproduce them with -fit off, or provide --verbose logs if the bug only occurs with -fit on)\n");
|
||||
|
||||
// the draft context is created from the same base params and follows the main context, fit both together
|
||||
const bool has_draft = params.speculative.has_dft();
|
||||
const bool spec_mtp = std::find(params.speculative.types.begin(), params.speculative.types.end(),
|
||||
COMMON_SPECULATIVE_TYPE_DRAFT_MTP) != params.speculative.types.end();
|
||||
|
||||
common_params params_dft = common_base_params_to_speculative(params);
|
||||
|
||||
auto mparams_dft = common_model_params_to_llama(params_dft);
|
||||
auto cparams_dft = common_context_params_to_llama(params_dft);
|
||||
if (spec_mtp) {
|
||||
cparams_dft.ctx_type = LLAMA_CONTEXT_TYPE_MTP;
|
||||
}
|
||||
cparams_dft.n_rs_seq = 0;
|
||||
|
||||
const common_fit_extra_model extra = {
|
||||
/*.path_model =*/ params_dft.model.path.c_str(),
|
||||
/*.mparams =*/ &mparams_dft,
|
||||
/*.cparams =*/ &cparams_dft,
|
||||
/*.shares_model =*/ !has_draft, // an MTP context runs on the weights of the main model
|
||||
};
|
||||
|
||||
common_fit_params(params.model.path.c_str(), &mparams, &cparams,
|
||||
params.tensor_split,
|
||||
params.tensor_buft_overrides.data(),
|
||||
params.fit_params_target.data(),
|
||||
params.fit_params_min_ctx,
|
||||
has_draft || spec_mtp ? &extra : nullptr,
|
||||
params.verbosity >= LOG_LEVEL_DEBUG ? GGML_LOG_LEVEL_DEBUG : GGML_LOG_LEVEL_ERROR);
|
||||
}
|
||||
common_fit_apply(params, mparams, cparams);
|
||||
|
||||
llama_model * model = llama_model_load_from_file(params.model.path.c_str(), mparams);
|
||||
if (model == NULL) {
|
||||
@@ -1291,13 +1333,17 @@ common_init_result::common_init_result(common_params & params, bool model_only)
|
||||
// these decision models return a score for each token via the embeddings output
|
||||
// TODO: maybe improve this in the future
|
||||
const auto decision_type = common_get_decision_type(model);
|
||||
// minicpm-ocr-jev reads its class scores from the pooled output, the pooling type is the one of the GGUF
|
||||
if (decision_type == COMMON_DECISION_TYPE_LAYA || decision_type == COMMON_DECISION_TYPE_KEV || decision_type == COMMON_DECISION_TYPE_CLEF ||
|
||||
decision_type == COMMON_DECISION_TYPE_LFM2_D1_OMNI || decision_type == COMMON_DECISION_TYPE_VLLM_DECISION2) {
|
||||
decision_type == COMMON_DECISION_TYPE_LFM2_D1_OMNI || decision_type == COMMON_DECISION_TYPE_VLLM_DECISION2 ||
|
||||
decision_type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV) {
|
||||
const auto pooling_type = decision_type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV ? LLAMA_POOLING_TYPE_UNSPECIFIED : LLAMA_POOLING_TYPE_NONE;
|
||||
|
||||
params.embedding = true;
|
||||
params.pooling_type = LLAMA_POOLING_TYPE_NONE;
|
||||
params.pooling_type = pooling_type;
|
||||
|
||||
cparams.embeddings = true;
|
||||
cparams.pooling_type = LLAMA_POOLING_TYPE_NONE;
|
||||
cparams.pooling_type = pooling_type;
|
||||
cparams.n_outputs_max = cparams.n_batch;
|
||||
cparams.n_outputs_max_per_seq = 1;
|
||||
|
||||
@@ -1444,7 +1490,7 @@ common_init_result_ptr common_init_from_params(common_params & params, bool mode
|
||||
if (params.control_vector_layer_start <= 0) params.control_vector_layer_start = 1;
|
||||
if (params.control_vector_layer_end <= 0) params.control_vector_layer_end = llama_model_n_layer(model);
|
||||
|
||||
const auto cvec = common_control_vector_load(params.control_vectors);
|
||||
const auto cvec = common_control_vector_load(params.control_vectors, llama_model_n_layer(model));
|
||||
if (cvec.n_embd == -1) {
|
||||
return res;
|
||||
}
|
||||
@@ -1966,7 +2012,7 @@ float common_embd_similarity_cos(const float * embd1, const float * embd2, int n
|
||||
// Control vector utils
|
||||
//
|
||||
|
||||
static common_control_vector_data common_control_vector_load_one(const common_control_vector_load_info & load_info) {
|
||||
static common_control_vector_data common_control_vector_load_one(const common_control_vector_load_info & load_info, int32_t n_layer) {
|
||||
common_control_vector_data result = { -1, {} };
|
||||
|
||||
ggml_context * ctx = nullptr;
|
||||
@@ -2007,6 +2053,10 @@ static common_control_vector_data common_control_vector_load_one(const common_co
|
||||
COM_ERR("invalid (zero) direction tensor layer index in %s\n", load_info.fname.c_str());
|
||||
result.n_embd = -1;
|
||||
break;
|
||||
} else if (layer_idx > n_layer) {
|
||||
COM_ERR("direction tensor layer index %d exceeds the model layer count %d in %s\n", layer_idx, n_layer, load_info.fname.c_str());
|
||||
result.n_embd = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
struct ggml_tensor * tensor = ggml_get_tensor(ctx, name.c_str());
|
||||
@@ -2030,10 +2080,10 @@ static common_control_vector_data common_control_vector_load_one(const common_co
|
||||
}
|
||||
|
||||
// extend if necessary - do not store data for layer 0 (it's not used)
|
||||
result.data.resize(std::max(result.data.size(), static_cast<size_t>(result.n_embd * layer_idx)), 0.0f);
|
||||
result.data.resize(std::max(result.data.size(), (size_t) result.n_embd * layer_idx), 0.0f);
|
||||
|
||||
const float * src = (const float *) tensor->data;
|
||||
float * dst = result.data.data() + result.n_embd * (layer_idx - 1); // layer 1 at [0]
|
||||
float * dst = result.data.data() + (size_t) result.n_embd * (layer_idx - 1); // layer 1 at [0]
|
||||
for (int j = 0; j < result.n_embd; j++) {
|
||||
dst[j] += src[j] * load_info.strength; // allows multiple directions for same layer in same file
|
||||
}
|
||||
@@ -2051,11 +2101,11 @@ static common_control_vector_data common_control_vector_load_one(const common_co
|
||||
return result;
|
||||
}
|
||||
|
||||
common_control_vector_data common_control_vector_load(const std::vector<common_control_vector_load_info> & load_infos) {
|
||||
common_control_vector_data common_control_vector_load(const std::vector<common_control_vector_load_info> & load_infos, int32_t n_layer) {
|
||||
common_control_vector_data result = { -1, {} };
|
||||
|
||||
for (const auto & info : load_infos) {
|
||||
auto cur = common_control_vector_load_one(info);
|
||||
auto cur = common_control_vector_load_one(info, n_layer);
|
||||
|
||||
if (cur.n_embd == -1) {
|
||||
result.n_embd = -1;
|
||||
|
||||
+19
-1
@@ -31,6 +31,8 @@
|
||||
#define DIRECTORY_SEPARATOR '/'
|
||||
#endif // _WIN32
|
||||
|
||||
#define COMMON_RECURSIVE_LIMIT 128
|
||||
|
||||
#define COM_DBG(fmt, ...) LOG_DBG("cmn %12.*s: " fmt, 12, __func__, __VA_ARGS__)
|
||||
#define COM_TRC(fmt, ...) LOG_TRC("cmn %12.*s: " fmt, 12, __func__, __VA_ARGS__)
|
||||
#define COM_INF(fmt, ...) LOG_INF("cmn %12.*s: " fmt, 12, __func__, __VA_ARGS__)
|
||||
@@ -642,6 +644,10 @@ struct common_params {
|
||||
int32_t cache_ram_mib = 8192; // -1 = no limit, 0 - disable, 1 = 1 MiB, etc.
|
||||
bool cache_ram_explicit = false; // set by --cache-ram, decision models disable the cache otherwise
|
||||
|
||||
// llama-connect params
|
||||
bool server_connect = false;
|
||||
std::string server_connect_code = "";
|
||||
|
||||
std::string public_path = ""; // NOLINT
|
||||
std::string api_prefix = ""; // NOLINT
|
||||
std::string chat_template = ""; // NOLINT
|
||||
@@ -695,6 +701,7 @@ struct common_params {
|
||||
std::string models_dir = ""; // directory containing models for the router server
|
||||
std::string models_preset = ""; // directory containing model presets for the router server
|
||||
int models_max = 4; // maximum number of models to load simultaneously
|
||||
int models_memory_margin = 0; // MiB of device memory to keep free when loading models (0 = no memory accounting)
|
||||
bool models_autoload = true; // automatically load models when requested via the router server
|
||||
std::string models_preset_hf = ""; // show a warning about remote presets on router loaded (if not empty)
|
||||
|
||||
@@ -765,6 +772,7 @@ struct common_params {
|
||||
|
||||
// TTS params
|
||||
std::string tts_lang = "";
|
||||
std::string tts_voice = "";
|
||||
std::string tts_speaker_file = "";
|
||||
|
||||
bool is_gen_docs = false; // whether we are running inside llama-gen-docs
|
||||
@@ -970,6 +978,11 @@ enum common_decision_type {
|
||||
COMMON_DECISION_TYPE_VLLM_D3, // same as pplx-decider, non-causal attention
|
||||
COMMON_DECISION_TYPE_LFM2_D1, // same as openjev, the labels depend on the question type
|
||||
COMMON_DECISION_TYPE_LFM2_D1_OMNI, // same as laya, other prompt layout
|
||||
COMMON_DECISION_TYPE_GRANITE_SWITCH, // same as openjev, run by the decision aLoRA of granite switch
|
||||
COMMON_DECISION_TYPE_H2O_LIGHTNING, // same as openjev, the labels are read after an answer prefill
|
||||
COMMON_DECISION_TYPE_MINICPM_OCR_JEV, // one score per class, the pooled embeddings output of the last token
|
||||
COMMON_DECISION_TYPE_LFM3_D1, // same as lfm2-d1, non-causal, label codes A..Z then AA, AB..
|
||||
COMMON_DECISION_TYPE_LFM3_D1_OMNI, // same as lfm3-d1
|
||||
COMMON_DECISION_TYPE_UNKNOWN, // a decision model of a type that is not supported
|
||||
};
|
||||
|
||||
@@ -979,6 +992,7 @@ common_decision_type common_get_decision_type(const struct llama_model * model);
|
||||
struct common_gguf_info {
|
||||
common_decision_type decision_type = COMMON_DECISION_TYPE_UNKNOWN; // UNKNOWN if the file is missing, unreadable, or invalid
|
||||
uint32_t n_ctx_train = 0; // 0 if unknown
|
||||
bool has_chat_tmpl = false; // has the default tokenizer.chat_template
|
||||
};
|
||||
|
||||
common_gguf_info common_get_gguf_info(const std::string & fname);
|
||||
@@ -1008,6 +1022,9 @@ common_init_result_ptr common_init_from_params(common_params & params, bool mode
|
||||
struct llama_model_params common_model_params_to_llama ( common_params & params);
|
||||
struct llama_context_params common_context_params_to_llama(const common_params & params);
|
||||
|
||||
// apply --fit to mparams and cparams, no-op if params.fit_params is false
|
||||
void common_fit_apply(common_params & params, struct llama_model_params & mparams, struct llama_context_params & cparams);
|
||||
|
||||
// clear LoRA adapters from context, then apply new list of adapters
|
||||
void common_set_adapter_lora(struct llama_context * ctx, std::vector<common_adapter_lora_info> & lora);
|
||||
|
||||
@@ -1220,8 +1237,9 @@ struct common_control_vector_load_info {
|
||||
};
|
||||
|
||||
// Load control vectors, scale each by strength, and add them together.
|
||||
// Direction layer indices must be in [1, n_layer].
|
||||
// On error, returns {-1, empty}
|
||||
common_control_vector_data common_control_vector_load(const std::vector<common_control_vector_load_info> & load_infos);
|
||||
common_control_vector_data common_control_vector_load(const std::vector<common_control_vector_load_info> & load_infos, int32_t n_layer);
|
||||
|
||||
//
|
||||
// Split utils
|
||||
|
||||
+252
-85
@@ -4,6 +4,7 @@
|
||||
#include "common.h"
|
||||
#include "log.h"
|
||||
#include "download.h"
|
||||
#include "file-lock.h"
|
||||
#include "hf-cache.h"
|
||||
#include "json.h"
|
||||
|
||||
@@ -39,6 +40,9 @@
|
||||
// downloader
|
||||
//
|
||||
|
||||
// the suffix of the file a download writes until it completes
|
||||
static const std::string PARTIAL_SUFFIX = ".downloadInProgress";
|
||||
|
||||
static void write_etag(const std::string & path, const std::string & etag) {
|
||||
const std::string etag_path = path + ".etag";
|
||||
fs_write_atomic(std::filesystem::u8path(etag_path), etag);
|
||||
@@ -74,6 +78,18 @@ std::pair<std::string, std::string> common_download_split_repo_tag(const std::st
|
||||
return {hf_repo, tag};
|
||||
}
|
||||
|
||||
// file name from url, without the query string
|
||||
static std::string url_get_filename(const std::string & url) {
|
||||
std::string filename = url;
|
||||
if (auto pos = filename.rfind('/'); pos != std::string::npos) {
|
||||
filename = filename.substr(pos + 1);
|
||||
}
|
||||
if (auto pos = filename.find('?'); pos != std::string::npos) {
|
||||
filename = filename.substr(0, pos);
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
class ProgressBar : public common_download_callback {
|
||||
static inline std::mutex mutex;
|
||||
static inline std::map<const ProgressBar *, int> lines;
|
||||
@@ -97,14 +113,8 @@ public:
|
||||
ProgressBar() = default;
|
||||
|
||||
void on_start(const common_download_progress & p) override {
|
||||
filename = p.url;
|
||||
filename = url_get_filename(p.url);
|
||||
|
||||
if (auto pos = filename.rfind('/'); pos != std::string::npos) {
|
||||
filename = filename.substr(pos + 1);
|
||||
}
|
||||
if (auto pos = filename.find('?'); pos != std::string::npos) {
|
||||
filename = filename.substr(0, pos);
|
||||
}
|
||||
for (size_t i = 0; i < filename.size(); ++i) {
|
||||
if ((filename[i] & 0xC0) != 0x80) {
|
||||
if (len++ == 39) {
|
||||
@@ -186,6 +196,13 @@ static bool common_pull_file(httplib::Client & cli,
|
||||
[&](const httplib::Response &response) {
|
||||
if (p.downloaded > 0 && response.status != 206) {
|
||||
LOG_WRN("%s: server did not respond with 206 Partial Content for a resume request. Status: %d\n", func, response.status);
|
||||
if (response.status == 416 || response.status == 200) {
|
||||
// the kept partial cannot be resumed: 416 for a complete or stale one,
|
||||
// 200 from a server that ignores the range; drop it, so the next
|
||||
// attempt starts over instead of wedging on the same answer
|
||||
ofs.close();
|
||||
remove(path_tmp.c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (p.downloaded == 0 && response.status != 200) {
|
||||
@@ -214,7 +231,7 @@ static bool common_pull_file(httplib::Client & cli,
|
||||
if (progress_step >= p.total / 1000 || p.downloaded == p.total) {
|
||||
if (callback) {
|
||||
callback->on_update(p);
|
||||
if (callback->is_cancelled()) {
|
||||
if (callback->is_cancelled() && (p.total == 0 || p.downloaded != p.total)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -247,15 +264,62 @@ static bool common_pull_file(httplib::Client & cli,
|
||||
static int common_download_file_single_online(const std::string & url,
|
||||
const std::string & path,
|
||||
const common_download_opts & opts,
|
||||
bool skip_etag) {
|
||||
bool skip_etag,
|
||||
const std::string & lock_path,
|
||||
const std::string & alt_path) {
|
||||
static const int max_attempts = 3;
|
||||
static const int retry_delay_seconds = 2;
|
||||
|
||||
const bool file_exists = std::filesystem::exists(std::filesystem::u8path(path));
|
||||
const std::string path_temporary = path + PARTIAL_SUFFIX;
|
||||
const std::string path_lock = lock_path.empty() ? path + ".lock" : lock_path;
|
||||
|
||||
common_download_progress p;
|
||||
p.url = url;
|
||||
|
||||
if (opts.callback) {
|
||||
opts.callback->on_start(p);
|
||||
}
|
||||
|
||||
// the completion callback may itself take the same lock (the HF cache
|
||||
// finalizes blobs under it), so every exit goes through finish()
|
||||
common_file_lock lock(path_lock);
|
||||
auto finish = [&](int status) {
|
||||
lock.close();
|
||||
if (opts.callback) {
|
||||
opts.callback->on_done(p, is_http_status_ok(status));
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
// alt_path is the HF snapshot: the blob may have been moved there when symlinks are unavailable
|
||||
auto cache_hit = [&] {
|
||||
return std::filesystem::exists(std::filesystem::u8path(path)) ||
|
||||
(!alt_path.empty() && std::filesystem::exists(std::filesystem::u8path(alt_path)));
|
||||
};
|
||||
|
||||
if (skip_etag && cache_hit()) {
|
||||
LOG_DBG("%s: using cached file: %s\n", __func__, path.c_str());
|
||||
return finish(304); // 304 Not Modified - fake cached response
|
||||
}
|
||||
|
||||
// take the lock before touching the cache, so two processes cannot race on the same file
|
||||
bool waited = false;
|
||||
if (!lock.acquire([&] {
|
||||
if (!waited) {
|
||||
LOG_INF("%s: file '%s' is being downloaded by another process, waiting...\n", __func__, url_get_filename(url).c_str());
|
||||
waited = true;
|
||||
}
|
||||
return !(opts.callback && opts.callback->is_cancelled());
|
||||
})) {
|
||||
return finish(-1);
|
||||
}
|
||||
|
||||
// under the lock: another process may have finished while we waited
|
||||
bool file_exists = cache_hit();
|
||||
|
||||
if (file_exists && skip_etag) {
|
||||
LOG_DBG("%s: using cached file: %s\n", __func__, path.c_str());
|
||||
return 304; // 304 Not Modified - fake cached response
|
||||
return finish(304); // 304 Not Modified - fake cached response
|
||||
}
|
||||
|
||||
auto [cli, parts] = common_http_client(url);
|
||||
@@ -284,9 +348,9 @@ static int common_download_file_single_online(const std::string & url,
|
||||
LOG_TRC("%s: HEAD failed, status: %d\n", __func__, head ? head->status : -1);
|
||||
if (file_exists) {
|
||||
LOG_TRC("%s: using cached file (HEAD failed): %s\n", __func__, path.c_str());
|
||||
return 304; // 304 Not Modified - fake cached response
|
||||
return finish(304); // 304 Not Modified - fake cached response
|
||||
}
|
||||
return head ? head->status : -1;
|
||||
return finish(head ? head->status : -1);
|
||||
}
|
||||
|
||||
std::string etag;
|
||||
@@ -294,8 +358,6 @@ static int common_download_file_single_online(const std::string & url,
|
||||
etag = head->get_header_value("ETag");
|
||||
}
|
||||
|
||||
common_download_progress p;
|
||||
p.url = url;
|
||||
if (head->has_header("Content-Length")) {
|
||||
try {
|
||||
p.total = std::stoull(head->get_header_value("Content-Length"));
|
||||
@@ -312,16 +374,16 @@ static int common_download_file_single_online(const std::string & url,
|
||||
if (file_exists) {
|
||||
if (etag.empty()) {
|
||||
LOG_DBG("%s: using cached file (no server etag): %s\n", __func__, path.c_str());
|
||||
return 304; // 304 Not Modified - fake cached response
|
||||
return finish(304); // 304 Not Modified - fake cached response
|
||||
}
|
||||
if (!last_etag.empty() && last_etag == etag) {
|
||||
LOG_DBG("%s: using cached file (same etag): %s\n", __func__, path.c_str());
|
||||
return 304; // 304 Not Modified - fake cached response
|
||||
return finish(304); // 304 Not Modified - fake cached response
|
||||
}
|
||||
// pass this point, the file exists but is different from the server version, so we need to redownload it
|
||||
if (remove(path.c_str()) != 0) {
|
||||
LOG_ERR("%s: unable to delete file: %s\n", __func__, path.c_str());
|
||||
return -1;
|
||||
return finish(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,11 +393,24 @@ static int common_download_file_single_online(const std::string & url,
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
const std::string path_temporary = path + ".downloadInProgress";
|
||||
int delay = retry_delay_seconds;
|
||||
|
||||
if (!skip_etag && !etag.empty()) {
|
||||
if (std::filesystem::exists(path_temporary) && read_etag(path_temporary) != etag) {
|
||||
LOG_WRN("%s: partial file does not match the server etag, discarding it: %s\n", __func__, path_temporary.c_str());
|
||||
if (remove(path_temporary.c_str()) != 0) {
|
||||
LOG_ERR("%s: unable to delete file: %s\n", __func__, path_temporary.c_str());
|
||||
return finish(-1);
|
||||
}
|
||||
}
|
||||
write_etag(path_temporary, etag);
|
||||
}
|
||||
|
||||
if (opts.callback) {
|
||||
opts.callback->on_start(p);
|
||||
// report the resume offset, so a resumed file does not start at 0
|
||||
p.downloaded = supports_ranges && std::filesystem::exists(path_temporary) ?
|
||||
std::filesystem::file_size(path_temporary) : 0;
|
||||
opts.callback->on_update(p);
|
||||
}
|
||||
|
||||
for (int i = 0; i < max_attempts; ++i) {
|
||||
@@ -372,27 +447,19 @@ static int common_download_file_single_online(const std::string & url,
|
||||
}
|
||||
if (!etag.empty() && !skip_etag) {
|
||||
write_etag(path, etag);
|
||||
remove((path_temporary + ".etag").c_str());
|
||||
}
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.callback) {
|
||||
opts.callback->on_done(p, success);
|
||||
}
|
||||
if (opts.callback && opts.callback->is_cancelled() &&
|
||||
std::filesystem::exists(path_temporary)) {
|
||||
if (remove(path_temporary.c_str()) != 0) {
|
||||
LOG_ERR("%s: unable to delete temporary file: %s\n", __func__, path_temporary.c_str());
|
||||
}
|
||||
}
|
||||
// a stopped download keeps path_temporary, so the next attempt resumes from it
|
||||
if (!success) {
|
||||
LOG_ERR("%s: download failed after %d attempts\n", __func__, max_attempts);
|
||||
return -1; // max attempts reached
|
||||
}
|
||||
|
||||
return head->status;
|
||||
return finish(success ? head->status : -1);
|
||||
}
|
||||
|
||||
std::pair<long, std::vector<char>> common_remote_get_content(const std::string & url,
|
||||
@@ -432,14 +499,16 @@ std::pair<long, std::vector<char>> common_remote_get_content(const std::string
|
||||
int common_download_file_single(const std::string & url,
|
||||
const std::string & path,
|
||||
const common_download_opts & opts,
|
||||
bool skip_etag) {
|
||||
bool skip_etag,
|
||||
const std::string & lock_path,
|
||||
const std::string & alt_path) {
|
||||
if (!opts.offline) {
|
||||
ProgressBar tty_cb;
|
||||
common_download_opts online_opts = opts;
|
||||
if (!online_opts.callback) {
|
||||
online_opts.callback = &tty_cb;
|
||||
}
|
||||
return common_download_file_single_online(url, path, online_opts, skip_etag);
|
||||
return common_download_file_single_online(url, path, online_opts, skip_etag, lock_path, alt_path);
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(std::filesystem::u8path(path))) {
|
||||
@@ -594,33 +663,65 @@ static hf_cache::hf_file find_best_sibling(const hf_cache::hf_files & files,
|
||||
return best;
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_mmproj(const hf_cache::hf_files & files,
|
||||
const std::string & model) {
|
||||
return find_best_sibling(files, model, "mmproj");
|
||||
static const std::vector<common_download_sidecar> sidecars = {
|
||||
{ "mmproj", "mmproj", false, &common_download_opts::download_mmproj, &common_download_hf_plan::mmproj },
|
||||
{ "mtp", "mtp-", true, &common_download_opts::download_mtp, &common_download_hf_plan::mtp },
|
||||
{ "dflash", "dflash-", true, &common_download_opts::download_dflash, &common_download_hf_plan::dflash },
|
||||
{ "eagle3", "eagle3-", true, &common_download_opts::download_eagle3, &common_download_hf_plan::eagle3 },
|
||||
{ "dspark", "dspark-", true, &common_download_opts::download_dspark, &common_download_hf_plan::dspark },
|
||||
};
|
||||
|
||||
const std::vector<common_download_sidecar> & common_download_sidecars() {
|
||||
return sidecars;
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_mtp(const hf_cache::hf_files & files,
|
||||
const std::string & model,
|
||||
const std::string & tag = "") {
|
||||
return find_best_sibling(files, model, "mtp-", tag);
|
||||
bool common_download_opts_add_sidecar(common_download_opts & opts, const std::string & kind) {
|
||||
for (const auto & s : sidecars) {
|
||||
if (kind == s.name) {
|
||||
opts.*s.opt = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_eagle3(const hf_cache::hf_files & files,
|
||||
const std::string & model,
|
||||
const std::string & tag = "") {
|
||||
return find_best_sibling(files, model, "eagle3-", tag);
|
||||
std::vector<std::string> common_download_plan_sidecars(const common_download_hf_plan & plan) {
|
||||
std::vector<std::string> out;
|
||||
for (const auto & s : sidecars) {
|
||||
if (!(plan.*s.file).path.empty()) {
|
||||
out.emplace_back(s.name);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_dflash(const hf_cache::hf_files & files,
|
||||
const std::string & model,
|
||||
const std::string & tag = "") {
|
||||
return find_best_sibling(files, model, "dflash-", tag);
|
||||
std::vector<common_download_partial> common_download_plan_partials(const common_download_hf_plan & plan) {
|
||||
std::vector<common_download_partial> out;
|
||||
|
||||
// a file in the cache resolves to its snapshot entry, a file still to download to its blob
|
||||
auto add = [&](const std::string & kind, const hf_cache::hf_file & f) {
|
||||
if (!f.local_path.empty() && f.local_path != f.final_path) {
|
||||
out.push_back({ kind, f.local_path + PARTIAL_SUFFIX });
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto & f : plan.model_files) {
|
||||
add("", f);
|
||||
}
|
||||
for (const auto & s : sidecars) {
|
||||
add(s.name, plan.*s.file);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_dspark(const hf_cache::hf_files & files,
|
||||
const std::string & model,
|
||||
const std::string & tag = "") {
|
||||
return find_best_sibling(files, model, "dspark-", tag);
|
||||
// true when `name` carries the keyword of a sidecar kind
|
||||
static bool name_is_sidecar(const std::string & name) {
|
||||
for (const auto & s : sidecars) {
|
||||
if (name.find(s.keyword) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool gguf_filename_is_model(const std::string & filepath) {
|
||||
@@ -633,12 +734,7 @@ static bool gguf_filename_is_model(const std::string & filepath) {
|
||||
filename = filename.substr(pos + 1);
|
||||
}
|
||||
|
||||
return filename.find("mmproj") == std::string::npos &&
|
||||
filename.find("imatrix") == std::string::npos &&
|
||||
filename.find("mtp-") == std::string::npos &&
|
||||
filename.find("eagle3-") == std::string::npos &&
|
||||
filename.find("dflash-") == std::string::npos &&
|
||||
filename.find("dspark-") == std::string::npos;
|
||||
return filename.find("imatrix") == std::string::npos && !name_is_sidecar(filename);
|
||||
}
|
||||
|
||||
static hf_cache::hf_file find_best_model(const hf_cache::hf_files & files,
|
||||
@@ -730,8 +826,11 @@ common_download_hf_plan common_download_get_hf_plan(const common_params_model &
|
||||
}
|
||||
} else {
|
||||
primary = find_best_model(all, tag);
|
||||
// a requested sidecar can resolve on its own, without a full model of the same tag
|
||||
if (primary.path.empty() && !opts.download_mtp && !opts.download_dflash && !opts.download_eagle3 && !opts.download_dspark) {
|
||||
// a requested draft can resolve on its own, without a full model of the same tag
|
||||
const bool want_draft = std::any_of(sidecars.begin(), sidecars.end(), [&](const common_download_sidecar & s) {
|
||||
return s.draft && opts.*s.opt;
|
||||
});
|
||||
if (primary.path.empty() && !want_draft) {
|
||||
LOG_ERR("%s: no GGUF files found in repository %s\n", __func__, repo.c_str());
|
||||
list_available_gguf_files(all);
|
||||
return plan;
|
||||
@@ -743,24 +842,22 @@ common_download_hf_plan common_download_get_hf_plan(const common_params_model &
|
||||
plan.model_files = get_split_files(all, primary);
|
||||
}
|
||||
|
||||
if (opts.download_mmproj && !primary.path.empty()) {
|
||||
plan.mmproj = find_best_mmproj(all, primary.path);
|
||||
}
|
||||
if (opts.download_mtp) {
|
||||
plan.mtp = find_best_mtp(all, primary.path, tag);
|
||||
}
|
||||
if (opts.download_dflash) {
|
||||
plan.dflash = find_best_dflash(all, primary.path, tag);
|
||||
}
|
||||
if (opts.download_eagle3) {
|
||||
plan.eagle3 = find_best_eagle3(all, primary.path, tag);
|
||||
}
|
||||
if (opts.download_dspark) {
|
||||
plan.dspark = find_best_dspark(all, primary.path, tag);
|
||||
// a draft resolves at the tag, the mmproj pairs with the model
|
||||
for (const auto & s : sidecars) {
|
||||
if (!(opts.*s.opt)) {
|
||||
continue;
|
||||
}
|
||||
if (s.draft) {
|
||||
plan.*s.file = find_best_sibling(all, primary.path, s.keyword, tag);
|
||||
} else if (!primary.path.empty()) {
|
||||
plan.*s.file = find_best_sibling(all, primary.path, s.keyword);
|
||||
}
|
||||
}
|
||||
|
||||
if (primary.path.empty() &&
|
||||
plan.mtp.local_path.empty() && plan.dflash.local_path.empty() && plan.eagle3.local_path.empty() && plan.dspark.local_path.empty()) {
|
||||
const bool has_draft = std::any_of(sidecars.begin(), sidecars.end(), [&](const common_download_sidecar & s) {
|
||||
return s.draft && !(plan.*s.file).local_path.empty();
|
||||
});
|
||||
if (primary.path.empty() && !has_draft) {
|
||||
LOG_ERR("%s: no GGUF files found in repository %s\n", __func__, repo.c_str());
|
||||
list_available_gguf_files(all);
|
||||
}
|
||||
@@ -773,7 +870,7 @@ void common_download_run_tasks(const std::vector<common_download_task> & tasks)
|
||||
for (const auto & task : tasks) {
|
||||
futures.push_back(std::async(std::launch::async,
|
||||
[&task]() {
|
||||
return common_download_file_single(task.url, task.local_path, task.opts, task.is_hf);
|
||||
return common_download_file_single(task.url, task.local_path, task.opts, task.is_hf, task.lock_path, task.alt_path);
|
||||
}
|
||||
));
|
||||
}
|
||||
@@ -926,6 +1023,11 @@ std::string common_docker_resolve_model(const std::string & docker) {
|
||||
}
|
||||
}
|
||||
|
||||
// a cached file that lists a model under its tag: the first split of a model, not a sidecar
|
||||
static bool is_listed_model(const gguf_split_info & split) {
|
||||
return split.index == 1 && !split.tag.empty() && !name_is_sidecar(split.prefix);
|
||||
}
|
||||
|
||||
std::vector<common_cached_model_info> common_list_cached_models() {
|
||||
std::unordered_set<std::string> seen;
|
||||
std::vector<common_cached_model_info> result;
|
||||
@@ -934,12 +1036,7 @@ std::vector<common_cached_model_info> common_list_cached_models() {
|
||||
|
||||
for (const auto & f : files) {
|
||||
auto split = get_gguf_split_info(f.path);
|
||||
if (split.index != 1 || split.tag.empty() ||
|
||||
split.prefix.find("mmproj") != std::string::npos ||
|
||||
split.prefix.find("mtp-") != std::string::npos ||
|
||||
split.prefix.find("eagle3-") != std::string::npos ||
|
||||
split.prefix.find("dflash-") != std::string::npos ||
|
||||
split.prefix.find("dspark-") != std::string::npos) {
|
||||
if (!is_listed_model(split)) {
|
||||
continue;
|
||||
}
|
||||
if (seen.insert(f.repo_id + ":" + split.tag).second) {
|
||||
@@ -970,6 +1067,44 @@ std::string common_download_resolve_path(const std::string & hf_repo_with_tag, c
|
||||
return find_best_model(files, tag).local_path;
|
||||
}
|
||||
|
||||
// the files -hf loads from the cache, the model with every sidecar, for each cached
|
||||
// tag of `repo_id` other than `tag`
|
||||
static std::unordered_set<std::string> files_in_use(const std::string & repo_id, const std::string & tag) {
|
||||
common_download_opts opts;
|
||||
opts.offline = true;
|
||||
for (const auto & s : sidecars) {
|
||||
opts.*s.opt = true;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> tags;
|
||||
std::unordered_set<std::string> used;
|
||||
|
||||
for (const auto & f : hf_cache::get_cached_files(repo_id)) {
|
||||
auto split = get_gguf_split_info(f.path);
|
||||
if (!is_listed_model(split) || split.tag == tag || !tags.insert(split.tag).second) {
|
||||
continue;
|
||||
}
|
||||
|
||||
common_params_model model;
|
||||
model.hf_repo = repo_id + ":" + split.tag;
|
||||
|
||||
auto plan = common_download_get_hf_plan(model, opts);
|
||||
|
||||
for (const auto & m : plan.model_files) {
|
||||
used.insert(m.local_path);
|
||||
}
|
||||
for (const auto & s : sidecars) {
|
||||
const auto & sf = plan.*s.file;
|
||||
if (!sf.local_path.empty()) {
|
||||
used.insert(sf.local_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return used;
|
||||
}
|
||||
|
||||
static bool remove_snapshot_entries(const std::string & repo_id, const std::vector<std::filesystem::path> & to_remove);
|
||||
|
||||
bool common_download_remove(const std::string & hf_repo_with_tag) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -989,11 +1124,13 @@ bool common_download_remove(const std::string & hf_repo_with_tag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// collect snapshot entries whose tag matches
|
||||
// collect snapshot entries whose tag matches, but a file another tag loads stays
|
||||
const auto used = files_in_use(repo_id, tag_upper);
|
||||
|
||||
std::vector<fs::path> to_remove;
|
||||
for (const auto & f : files) {
|
||||
auto split = get_gguf_split_info(f.path);
|
||||
if (split.tag == tag_upper) {
|
||||
if (split.tag == tag_upper && used.find(f.local_path) == used.end()) {
|
||||
to_remove.emplace_back(f.local_path);
|
||||
}
|
||||
}
|
||||
@@ -1002,6 +1139,13 @@ bool common_download_remove(const std::string & hf_repo_with_tag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return remove_snapshot_entries(repo_id, to_remove);
|
||||
}
|
||||
|
||||
// remove snapshot entries of `repo_id`, then the blobs no remaining entry references
|
||||
static bool remove_snapshot_entries(const std::string & repo_id, const std::vector<std::filesystem::path> & to_remove) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// resolve blob paths from symlinks before deleting snapshot entries
|
||||
std::vector<fs::path> blobs_to_check;
|
||||
for (const auto & p : to_remove) {
|
||||
@@ -1053,3 +1197,26 @@ bool common_download_remove(const std::string & hf_repo_with_tag) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool common_download_remove_sidecars(const std::string & hf_repo_with_tag, const common_download_opts & opts) {
|
||||
common_params_model model;
|
||||
model.hf_repo = hf_repo_with_tag;
|
||||
|
||||
common_download_opts cached = opts;
|
||||
cached.offline = true;
|
||||
|
||||
auto plan = common_download_get_hf_plan(model, cached);
|
||||
|
||||
std::vector<std::filesystem::path> to_remove;
|
||||
for (const auto & s : sidecars) {
|
||||
const auto & f = plan.*s.file;
|
||||
if (!f.local_path.empty()) {
|
||||
to_remove.emplace_back(std::filesystem::u8path(f.local_path));
|
||||
}
|
||||
}
|
||||
if (to_remove.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return remove_snapshot_entries(common_download_split_repo_tag(hf_repo_with_tag).first, to_remove);
|
||||
}
|
||||
|
||||
+47
-3
@@ -69,12 +69,15 @@ struct common_download_task {
|
||||
std::string local_path;
|
||||
std::function<void()> on_done;
|
||||
bool is_hf = false;
|
||||
std::string lock_path;
|
||||
std::string alt_path; // HF snapshot path, also treated as cached (the blob may be moved there)
|
||||
|
||||
common_download_task() = default;
|
||||
common_download_task(hf_cache::hf_file f,
|
||||
const common_download_opts & opts,
|
||||
std::function<void()> on_done = nullptr)
|
||||
: opts(opts), url(f.url), local_path(f.local_path), on_done(on_done), is_hf(true) {}
|
||||
: opts(opts), url(f.url), local_path(f.local_path), on_done(on_done), is_hf(true),
|
||||
lock_path(hf_cache::get_lock_path(f.repo_id, f.oid)), alt_path(f.final_path) {}
|
||||
};
|
||||
|
||||
void common_download_run_tasks(const std::vector<common_download_task> & tasks);
|
||||
@@ -92,10 +95,14 @@ std::string common_download_resolve_path(const std::string & hf_repo_with_tag, c
|
||||
// download single file from url to local path
|
||||
// returns status code or -1 on error
|
||||
// skip_etag: if true, don't read/write .etag files (for HF cache where filename is the hash)
|
||||
// lock_path: lock file used to serialize downloads of the same file; when empty, "<path>.lock" is used
|
||||
// alt_path: additional path treated as present in cache (see cache_hit in download.cpp)
|
||||
int common_download_file_single(const std::string & url,
|
||||
const std::string & path,
|
||||
const common_download_opts & opts = {},
|
||||
bool skip_etag = false);
|
||||
bool skip_etag = false,
|
||||
const std::string & lock_path = "",
|
||||
const std::string & alt_path = "");
|
||||
|
||||
// resolve and download model from Docker registry
|
||||
// return local path to downloaded model file
|
||||
@@ -104,10 +111,17 @@ std::string common_docker_resolve_model(const std::string & docker);
|
||||
// Remove a cached model from disk
|
||||
// input format: "user/model" or "user/model:tag"
|
||||
// - if tag is omitted, removes the entire repo cache directory
|
||||
// - if tag is present, removes only files matching that tag (and orphaned blobs)
|
||||
// - if tag is present, removes only files matching that tag (and orphaned blobs),
|
||||
// except the files another cached tag of the repo still loads
|
||||
// returns true if anything was removed
|
||||
bool common_download_remove(const std::string & hf_repo_with_tag);
|
||||
|
||||
// remove the cached sidecar files `opts` selects for "user/model:tag", as the cached
|
||||
// plan resolves them, and leave the model files in place; a sidecar file another tag
|
||||
// shares goes for every tag that resolves it
|
||||
// returns true if anything was removed
|
||||
bool common_download_remove_sidecars(const std::string & hf_repo_with_tag, const common_download_opts & opts);
|
||||
|
||||
struct common_download_hf_plan {
|
||||
hf_cache::hf_file primary;
|
||||
hf_cache::hf_files model_files;
|
||||
@@ -119,3 +133,33 @@ struct common_download_hf_plan {
|
||||
hf_cache::hf_file preset; // if set, only this file is downloaded
|
||||
};
|
||||
common_download_hf_plan common_download_get_hf_plan(const common_params_model & model, const common_download_opts & opts);
|
||||
|
||||
// a sidecar kind a download can pull next to its model: mmproj, and the draft heads
|
||||
// of --spec-type draft-<name>
|
||||
struct common_download_sidecar {
|
||||
const char * name; // the kind, as the API names it
|
||||
const char * keyword; // the part of a file name that marks a file of this kind
|
||||
bool draft; // a draft head, resolved at the tag even without a model
|
||||
|
||||
bool common_download_opts::* opt; // the option that requests it
|
||||
hf_cache::hf_file common_download_hf_plan::* file; // the plan file it resolves to
|
||||
};
|
||||
|
||||
// every sidecar kind, the single list the resolution, the listing and the removal follow
|
||||
const std::vector<common_download_sidecar> & common_download_sidecars();
|
||||
|
||||
// select the sidecar `kind` in `opts`; returns false for an unknown kind
|
||||
bool common_download_opts_add_sidecar(common_download_opts & opts, const std::string & kind);
|
||||
|
||||
// the sidecar kinds `plan` resolves a file for
|
||||
std::vector<std::string> common_download_plan_sidecars(const common_download_hf_plan & plan);
|
||||
|
||||
// a file `plan` still has to download, at the path its partial takes while it is
|
||||
// incomplete; kind is the sidecar it belongs to, empty for the model files
|
||||
struct common_download_partial {
|
||||
std::string kind;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
// the partials the download of `plan` writes, for the files not in the cache yet
|
||||
std::vector<common_download_partial> common_download_plan_partials(const common_download_hf_plan & plan);
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#include "file-lock.h"
|
||||
|
||||
#include "log.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
common_file_lock::common_file_lock(const std::string & lock_path) {
|
||||
this->lock_path = lock_path;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
// the \\?\ prefix (for paths longer than 255 chars) is only accepted on absolute paths
|
||||
// with backslash separators: normalize first, then check the length of the absolute form
|
||||
static std::wstring as_extended_path(const std::string & path) {
|
||||
// convert from the narrow encoding std::filesystem uses on this system
|
||||
int wlen = MultiByteToWideChar(CP_ACP, 0, path.c_str(), (int) path.size(), nullptr, 0);
|
||||
if (wlen <= 0) {
|
||||
return std::wstring(path.begin(), path.end()); // best effort for exotic encodings
|
||||
}
|
||||
std::wstring wide(wlen, L'\0');
|
||||
MultiByteToWideChar(CP_ACP, 0, path.c_str(), (int) path.size(), wide.data(), wlen);
|
||||
|
||||
if (wide.rfind(L"\\\\?\\", 0) == 0) {
|
||||
return wide;
|
||||
}
|
||||
|
||||
// GetFullPathNameW resolves relative paths and normalizes separators
|
||||
DWORD size = GetFullPathNameW(wide.c_str(), 0, nullptr, nullptr);
|
||||
if (size == 0) {
|
||||
return wide;
|
||||
}
|
||||
std::wstring full(size, L'\0');
|
||||
DWORD written = GetFullPathNameW(wide.c_str(), size, full.data(), nullptr);
|
||||
if (written == 0 || written >= size) {
|
||||
return wide;
|
||||
}
|
||||
full.resize(written);
|
||||
|
||||
if (full.size() <= 255) {
|
||||
return wide; // short enough: open the path exactly as given
|
||||
}
|
||||
if (full.rfind(L"\\\\", 0) == 0) {
|
||||
return L"\\\\?\\UNC\\" + full.substr(2); // \\server\share -> \\?\UNC\server\share
|
||||
}
|
||||
return L"\\\\?\\" + full;
|
||||
}
|
||||
#endif
|
||||
|
||||
common_file_lock::status common_file_lock::try_lock() {
|
||||
if (lock_path.empty()) {
|
||||
return status::acquired; // no lock path, nothing to lock
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
if (fd < 0) {
|
||||
// opened with a wide path: extended paths cannot go through the narrow CRT
|
||||
std::wstring lock_path_w = as_extended_path(lock_path);
|
||||
fd = _wopen(lock_path_w.c_str(), _O_RDWR | _O_CREAT | _O_BINARY | _O_NOINHERIT, _S_IREAD | _S_IWRITE);
|
||||
if (fd < 0) {
|
||||
LOG_ERR("%s: cannot open lock file: %s\n", __func__, lock_path.c_str());
|
||||
return status::error;
|
||||
}
|
||||
}
|
||||
// lock the first byte, the file itself stays empty
|
||||
OVERLAPPED ov = {};
|
||||
locked = LockFileEx((HANDLE) _get_osfhandle(fd),
|
||||
LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &ov);
|
||||
if (locked) {
|
||||
return status::acquired;
|
||||
}
|
||||
if (GetLastError() == ERROR_LOCK_VIOLATION) {
|
||||
return status::busy;
|
||||
}
|
||||
LOG_ERR("%s: file locking failed (%d)\n", __func__, (int) GetLastError());
|
||||
return status::error;
|
||||
#else
|
||||
if (fd < 0) {
|
||||
int open_flags = O_RDWR | O_CREAT | O_CLOEXEC;
|
||||
#ifdef O_NOFOLLOW
|
||||
open_flags |= O_NOFOLLOW;
|
||||
#endif
|
||||
// group-writable, so users sharing a cache can wait on the lock
|
||||
fd = open(lock_path.c_str(), open_flags, 0664);
|
||||
if (fd < 0) {
|
||||
LOG_ERR("%s: cannot open lock file: %s\n", __func__, lock_path.c_str());
|
||||
return status::error;
|
||||
}
|
||||
// open() filters the mode through the umask
|
||||
(void) fchmod(fd, 0664);
|
||||
}
|
||||
|
||||
if (flock(fd, LOCK_EX | LOCK_NB) == 0) {
|
||||
locked = true;
|
||||
return status::acquired;
|
||||
}
|
||||
// EACCES is reported as contention on some filesystems
|
||||
if (errno == EWOULDBLOCK || errno == EAGAIN || errno == EACCES) {
|
||||
return status::busy;
|
||||
}
|
||||
// flock is not implemented (some NFS/FUSE mounts): report it instead of racing
|
||||
LOG_ERR("%s: file locking failed (%s)\n", __func__, strerror(errno));
|
||||
return status::error;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool common_file_lock::acquire(const std::function<bool()> & keep_waiting) {
|
||||
// the lock file is opened by try_lock, its parent must exist by then
|
||||
if (!lock_path.empty()) {
|
||||
std::error_code ec;
|
||||
auto lock_dir = std::filesystem::path(lock_path).parent_path();
|
||||
if (!lock_dir.empty()) {
|
||||
std::filesystem::create_directories(lock_dir, ec);
|
||||
if (ec) {
|
||||
LOG_ERR("%s: unable to create lock directory '%s': %s\n", __func__, lock_dir.string().c_str(), ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
switch (try_lock()) {
|
||||
case status::acquired:
|
||||
return true;
|
||||
case status::busy:
|
||||
if (keep_waiting && !keep_waiting()) {
|
||||
return false;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
case status::error:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the lock file is never deleted: another process could then create a fresh
|
||||
// file at the same path and lock that
|
||||
void common_file_lock::close() {
|
||||
#if defined(_WIN32)
|
||||
if (fd >= 0) {
|
||||
if (locked) {
|
||||
OVERLAPPED ov = {};
|
||||
UnlockFileEx((HANDLE) _get_osfhandle(fd), 0, 1, 0, &ov);
|
||||
}
|
||||
_close(fd);
|
||||
}
|
||||
#else
|
||||
if (fd >= 0) {
|
||||
if (locked) {
|
||||
flock(fd, LOCK_UN);
|
||||
}
|
||||
::close(fd);
|
||||
}
|
||||
#endif
|
||||
fd = -1;
|
||||
locked = false;
|
||||
}
|
||||
|
||||
common_file_lock::~common_file_lock() {
|
||||
close();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
// Cross-process exclusive lock (flock on POSIX, LockFileEx on Windows).
|
||||
// Lazy: acquire() opens the lock file, so a read-only cache works if the lock is never acquired.
|
||||
class common_file_lock {
|
||||
public:
|
||||
common_file_lock(const std::string & lock_path);
|
||||
~common_file_lock();
|
||||
|
||||
// take the lock, waiting while another process holds it;
|
||||
// keep_waiting is polled before each retry, false cancels the wait.
|
||||
// Also returns false when locking is unavailable.
|
||||
bool acquire(const std::function<bool()> & keep_waiting = {});
|
||||
|
||||
void close();
|
||||
|
||||
common_file_lock(const common_file_lock &) = delete;
|
||||
common_file_lock & operator=(const common_file_lock &) = delete;
|
||||
|
||||
private:
|
||||
enum class status {
|
||||
acquired,
|
||||
busy, // another process holds the lock
|
||||
error, // file locking is unavailable: no exclusion is possible
|
||||
};
|
||||
|
||||
status try_lock();
|
||||
|
||||
std::string lock_path;
|
||||
|
||||
int fd = -1;
|
||||
bool locked = false;
|
||||
};
|
||||
+197
-103
@@ -1,6 +1,8 @@
|
||||
#include "fit.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "json.h"
|
||||
#include "llama-cpp.h"
|
||||
#include "log.h"
|
||||
|
||||
#include "../src/llama-ext.h"
|
||||
@@ -27,6 +29,32 @@ class common_params_fit_exception : public std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
// RAII: send llama logs below min_level to the debug log
|
||||
struct common_fit_log_scope {
|
||||
common_fit_log_scope(ggml_log_level min_level) : min_level(min_level) {
|
||||
llama_log_get(&original_logger.callback, &original_logger.user_data);
|
||||
llama_log_set([](ggml_log_level level, const char * text, void * user_data) {
|
||||
const common_fit_log_scope * self = (const common_fit_log_scope *) user_data;
|
||||
const ggml_log_level level_eff = level >= self->min_level ? level : GGML_LOG_LEVEL_DEBUG;
|
||||
self->original_logger.callback(level_eff, text, self->original_logger.user_data);
|
||||
}, this);
|
||||
}
|
||||
|
||||
~common_fit_log_scope() {
|
||||
llama_log_set(original_logger.callback, original_logger.user_data);
|
||||
}
|
||||
|
||||
common_fit_log_scope(const common_fit_log_scope &) = delete;
|
||||
common_fit_log_scope & operator=(const common_fit_log_scope &) = delete;
|
||||
|
||||
private:
|
||||
struct {
|
||||
ggml_log_callback callback;
|
||||
void * user_data;
|
||||
} original_logger;
|
||||
ggml_log_level min_level;
|
||||
};
|
||||
|
||||
static std::vector<llama_device_memory_data> common_get_device_memory_data_impl(
|
||||
const char * path_model,
|
||||
const llama_model_params * mparams,
|
||||
@@ -36,22 +64,7 @@ static std::vector<llama_device_memory_data> common_get_device_memory_data_impl(
|
||||
uint32_t & hp_n_ctx_train,
|
||||
uint32_t & hp_n_expert,
|
||||
ggml_log_level log_level) {
|
||||
struct user_data_t {
|
||||
struct {
|
||||
ggml_log_callback callback;
|
||||
void * user_data;
|
||||
} original_logger;
|
||||
ggml_log_level min_level; // prints below this log level go to debug log
|
||||
};
|
||||
user_data_t ud;
|
||||
llama_log_get(&ud.original_logger.callback, &ud.original_logger.user_data);
|
||||
ud.min_level = log_level;
|
||||
|
||||
llama_log_set([](ggml_log_level level, const char * text, void * user_data) {
|
||||
const user_data_t * ud = (const user_data_t *) user_data;
|
||||
const ggml_log_level level_eff = level >= ud->min_level ? level : GGML_LOG_LEVEL_DEBUG;
|
||||
ud->original_logger.callback(level_eff, text, ud->original_logger.user_data);
|
||||
}, &ud);
|
||||
common_fit_log_scope log_scope(log_level);
|
||||
|
||||
llama_model_params mparams_copy = *mparams;
|
||||
mparams_copy.no_alloc = true;
|
||||
@@ -59,14 +72,12 @@ static std::vector<llama_device_memory_data> common_get_device_memory_data_impl(
|
||||
|
||||
llama_model * model = llama_model_load_from_file(path_model, mparams_copy);
|
||||
if (model == nullptr) {
|
||||
llama_log_set(ud.original_logger.callback, ud.original_logger.user_data);
|
||||
throw std::runtime_error("failed to load model");
|
||||
}
|
||||
|
||||
llama_context * ctx = llama_init_from_model(model, *cparams);
|
||||
if (ctx == nullptr) {
|
||||
llama_model_free(model);
|
||||
llama_log_set(ud.original_logger.callback, ud.original_logger.user_data);
|
||||
throw std::runtime_error("failed to create llama_context from model");
|
||||
}
|
||||
|
||||
@@ -148,11 +159,81 @@ static std::vector<llama_device_memory_data> common_get_device_memory_data_impl(
|
||||
|
||||
llama_free(ctx);
|
||||
llama_model_free(model);
|
||||
llama_log_set(ud.original_logger.callback, ud.original_logger.user_data);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<common_memory_breakdown_row> common_fit_measure(
|
||||
const char * path_model,
|
||||
const llama_model_params * mparams,
|
||||
const llama_context_params * cparams,
|
||||
uint32_t * n_ctx_train,
|
||||
uint32_t * n_ctx,
|
||||
ggml_log_level log_level) {
|
||||
common_fit_log_scope log_scope(log_level);
|
||||
|
||||
llama_model_params mparams_copy = *mparams;
|
||||
mparams_copy.no_alloc = true;
|
||||
mparams_copy.load_mode = LLAMA_LOAD_MODE_NONE;
|
||||
|
||||
llama_model_ptr model(llama_model_load_from_file(path_model, mparams_copy));
|
||||
if (!model) {
|
||||
throw std::runtime_error("failed to load model");
|
||||
}
|
||||
|
||||
llama_context_ptr ctx(llama_init_from_model(model.get(), *cparams));
|
||||
if (!ctx) {
|
||||
throw std::runtime_error("failed to create llama_context from model");
|
||||
}
|
||||
|
||||
if (n_ctx_train) {
|
||||
*n_ctx_train = llama_model_n_ctx_train(model.get());
|
||||
}
|
||||
if (n_ctx) {
|
||||
*n_ctx = llama_n_ctx(ctx.get());
|
||||
}
|
||||
|
||||
return common_memory_breakdown_rows(ctx.get());
|
||||
}
|
||||
|
||||
common_memory_reports common_measure_memory(common_params & params) {
|
||||
const ggml_log_level log_level = params.verbosity >= LOG_LEVEL_DEBUG ? GGML_LOG_LEVEL_DEBUG : GGML_LOG_LEVEL_ERROR;
|
||||
const char * path_model = params.model.path.c_str();
|
||||
|
||||
common_memory_reports res;
|
||||
|
||||
auto mparams = common_model_params_to_llama(params);
|
||||
auto cparams = common_context_params_to_llama(params);
|
||||
|
||||
// max: what --fit picks now, no-op with -fit off
|
||||
{
|
||||
auto mparams_fit = mparams;
|
||||
auto cparams_fit = cparams;
|
||||
common_fit_apply(params, mparams_fit, cparams_fit);
|
||||
res.info_max = common_fit_measure(path_model, &mparams_fit, &cparams_fit, &res.n_ctx_train, &res.n_ctx_max, log_level);
|
||||
}
|
||||
|
||||
// min: the smallest context --fit may pick (--fit only changes n_ctx when it is 0)
|
||||
uint32_t n_ctx_min = cparams.n_ctx;
|
||||
if (n_ctx_min == 0 && params.fit_params) {
|
||||
const uint32_t n_streams = cparams.kv_unified ? 1 : std::max<uint32_t>(1, cparams.n_seq_max);
|
||||
// -c 0 sets fit_params_min_ctx to UINT32_MAX, cap at n_ctx_train like --fit
|
||||
n_ctx_min = (uint32_t) std::min<uint64_t>(
|
||||
uint64_t(params.fit_params_min_ctx) * n_streams,
|
||||
uint64_t(res.n_ctx_train) * n_streams);
|
||||
}
|
||||
if (n_ctx_min == 0 || n_ctx_min >= res.n_ctx_max) {
|
||||
// nothing to reduce, or -fit off with the trained context
|
||||
res.n_ctx_min = res.n_ctx_max;
|
||||
res.info_min = res.info_max;
|
||||
} else {
|
||||
cparams.n_ctx = n_ctx_min;
|
||||
res.info_min = common_fit_measure(path_model, &mparams, &cparams, nullptr, &res.n_ctx_min, log_level);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
common_device_memory_data_vec common_get_device_memory_data(
|
||||
const char * path_model,
|
||||
const llama_model_params * mparams,
|
||||
@@ -903,8 +984,7 @@ enum common_params_fit_status common_fit_params(
|
||||
return status;
|
||||
}
|
||||
|
||||
void common_memory_breakdown_print(const struct llama_context * ctx) {
|
||||
//const auto & devices = ctx->get_model().devices;
|
||||
std::vector<common_memory_breakdown_row> common_memory_breakdown_rows(const struct llama_context * ctx) {
|
||||
const auto * model = llama_get_model(ctx);
|
||||
|
||||
std::vector<ggml_backend_dev_t> devices;
|
||||
@@ -914,18 +994,6 @@ void common_memory_breakdown_print(const struct llama_context * ctx) {
|
||||
|
||||
llama_memory_breakdown memory_breakdown = llama_get_memory_breakdown(ctx);
|
||||
|
||||
std::vector<std::array<std::string, 9>> table_data;
|
||||
table_data.reserve(devices.size());
|
||||
|
||||
// same data as the table below, for --log-jsonl consumers
|
||||
common_json rows = common_json::array();
|
||||
const std::string template_header = "%s: | %s | %s %s %s %s %s %s %s |\n";
|
||||
const std::string template_gpu = "%s: | %s | %s = %s + (%s = %s + %s + %s) + %s |\n";
|
||||
const std::string template_other = "%s: | %s | %s %s %s = %s + %s + %s %s |\n";
|
||||
|
||||
table_data.push_back({template_header, "memory breakdown [MiB]", "total", "free", "self", "model", "context", "compute", "unaccounted"});
|
||||
|
||||
constexpr size_t MiB = 1024 * 1024;
|
||||
const std::vector<std::string> desc_prefixes_strip = {"NVIDIA ", "GeForce ", "Tesla ", "AMD ", "Radeon ", "Instinct "};
|
||||
|
||||
// track seen buffer types to avoid double counting:
|
||||
@@ -964,104 +1032,130 @@ void common_memory_breakdown_print(const struct llama_context * ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
// print memory breakdown for each device:
|
||||
std::vector<common_memory_breakdown_row> rows;
|
||||
rows.reserve(devices.size() + 1 + memory_breakdown.size());
|
||||
|
||||
for (size_t i = 0; i < devices.size(); i++) {
|
||||
ggml_backend_dev_t dev = devices[i];
|
||||
llama_memory_breakdown_data mb = mb_dev[i];
|
||||
|
||||
const std::string name = ggml_backend_dev_name(dev);
|
||||
std::string desc = ggml_backend_dev_description(dev);
|
||||
common_memory_breakdown_row row;
|
||||
row.kind = common_memory_breakdown_row::DEVICE;
|
||||
row.name = ggml_backend_dev_name(dev);
|
||||
row.description = ggml_backend_dev_description(dev);
|
||||
for (const std::string & prefix : desc_prefixes_strip) {
|
||||
if (desc.length() >= prefix.length() && desc.substr(0, prefix.length()) == prefix) {
|
||||
desc = desc.substr(prefix.length());
|
||||
if (row.description.length() >= prefix.length() && row.description.substr(0, prefix.length()) == prefix) {
|
||||
row.description = row.description.substr(prefix.length());
|
||||
}
|
||||
}
|
||||
|
||||
size_t free, total;
|
||||
ggml_backend_dev_memory(dev, &free, &total);
|
||||
|
||||
const size_t self = mb.model + mb.context + mb.compute;
|
||||
const int64_t unaccounted = static_cast<int64_t>(total) - static_cast<int64_t>(free) - static_cast<int64_t>(self);
|
||||
|
||||
table_data.push_back({
|
||||
template_gpu,
|
||||
" - " + name + " (" + desc + ")",
|
||||
std::to_string(total / MiB),
|
||||
std::to_string(free / MiB),
|
||||
std::to_string(self / MiB),
|
||||
std::to_string(mb.model / MiB),
|
||||
std::to_string(mb.context / MiB),
|
||||
std::to_string(mb.compute / MiB),
|
||||
std::to_string(unaccounted / static_cast<int64_t>(MiB))});
|
||||
|
||||
rows.push_back({
|
||||
{"kind", "device"},
|
||||
{"name", name},
|
||||
{"description", desc},
|
||||
{"total", total / MiB},
|
||||
{"free", free / MiB},
|
||||
{"self", self / MiB},
|
||||
{"model", mb.model / MiB},
|
||||
{"context", mb.context / MiB},
|
||||
{"compute", mb.compute / MiB},
|
||||
{"unaccounted", unaccounted / static_cast<int64_t>(MiB)},
|
||||
});
|
||||
row.total = total;
|
||||
row.free = free;
|
||||
row.model = mb_dev[i].model;
|
||||
row.context = mb_dev[i].context;
|
||||
row.compute = mb_dev[i].compute;
|
||||
rows.push_back(std::move(row));
|
||||
}
|
||||
|
||||
// print memory breakdown for host:
|
||||
{
|
||||
const size_t self = mb_host.model + mb_host.context + mb_host.compute;
|
||||
table_data.push_back({
|
||||
template_other,
|
||||
" - Host",
|
||||
"", // total
|
||||
"", // free
|
||||
std::to_string(self / MiB),
|
||||
std::to_string(mb_host.model / MiB),
|
||||
std::to_string(mb_host.context / MiB),
|
||||
std::to_string(mb_host.compute / MiB),
|
||||
""}); // unaccounted
|
||||
|
||||
rows.push_back({
|
||||
{"kind", "host"},
|
||||
{"name", "Host"},
|
||||
{"self", self / MiB},
|
||||
{"model", mb_host.model / MiB},
|
||||
{"context", mb_host.context / MiB},
|
||||
{"compute", mb_host.compute / MiB},
|
||||
});
|
||||
common_memory_breakdown_row row;
|
||||
row.kind = common_memory_breakdown_row::HOST;
|
||||
row.name = "Host";
|
||||
row.model = mb_host.model;
|
||||
row.context = mb_host.context;
|
||||
row.compute = mb_host.compute;
|
||||
rows.push_back(std::move(row));
|
||||
}
|
||||
|
||||
// print memory breakdown for all remaining buffer types:
|
||||
// all remaining buffer types:
|
||||
for (const auto & buft_mb : memory_breakdown) {
|
||||
ggml_backend_buffer_type_t buft = buft_mb.first;
|
||||
const llama_memory_breakdown_data & mb = buft_mb.second;
|
||||
if (seen_buffer_types.count(buft) == 1) {
|
||||
continue;
|
||||
}
|
||||
const std::string name = ggml_backend_buft_name(buft);
|
||||
const size_t self = mb.model + mb.context + mb.compute;
|
||||
common_memory_breakdown_row row;
|
||||
row.kind = common_memory_breakdown_row::BUFFER_TYPE;
|
||||
row.name = ggml_backend_buft_name(buft);
|
||||
row.model = mb.model;
|
||||
row.context = mb.context;
|
||||
row.compute = mb.compute;
|
||||
rows.push_back(std::move(row));
|
||||
|
||||
seen_buffer_types.insert(buft);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
void common_memory_breakdown_print(const struct llama_context * ctx) {
|
||||
const std::vector<common_memory_breakdown_row> rows = common_memory_breakdown_rows(ctx);
|
||||
|
||||
std::vector<std::array<std::string, 9>> table_data;
|
||||
table_data.reserve(rows.size() + 1);
|
||||
|
||||
// same data as the table below, for --log-jsonl consumers
|
||||
common_json json_rows = common_json::array();
|
||||
const std::string template_header = "%s: | %s | %s %s %s %s %s %s %s |\n";
|
||||
const std::string template_gpu = "%s: | %s | %s = %s + (%s = %s + %s + %s) + %s |\n";
|
||||
const std::string template_other = "%s: | %s | %s %s %s = %s + %s + %s %s |\n";
|
||||
|
||||
table_data.push_back({template_header, "memory breakdown [MiB]", "total", "free", "self", "model", "context", "compute", "unaccounted"});
|
||||
|
||||
constexpr size_t MiB = 1024 * 1024;
|
||||
|
||||
for (const auto & row : rows) {
|
||||
const size_t self = row.model + row.context + row.compute;
|
||||
|
||||
if (row.kind == common_memory_breakdown_row::DEVICE) {
|
||||
const int64_t unaccounted = row.total - row.free - static_cast<int64_t>(self);
|
||||
|
||||
table_data.push_back({
|
||||
template_gpu,
|
||||
" - " + row.name + " (" + row.description + ")",
|
||||
std::to_string(row.total / MiB),
|
||||
std::to_string(row.free / MiB),
|
||||
std::to_string(self / MiB),
|
||||
std::to_string(row.model / MiB),
|
||||
std::to_string(row.context / MiB),
|
||||
std::to_string(row.compute / MiB),
|
||||
std::to_string(unaccounted / static_cast<int64_t>(MiB))});
|
||||
|
||||
json_rows.push_back({
|
||||
{"kind", "device"},
|
||||
{"name", row.name},
|
||||
{"description", row.description},
|
||||
{"total", row.total / MiB},
|
||||
{"free", row.free / MiB},
|
||||
{"self", self / MiB},
|
||||
{"model", row.model / MiB},
|
||||
{"context", row.context / MiB},
|
||||
{"compute", row.compute / MiB},
|
||||
{"unaccounted", unaccounted / static_cast<int64_t>(MiB)},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
table_data.push_back({
|
||||
template_other,
|
||||
" - " + name,
|
||||
" - " + row.name,
|
||||
"", // total
|
||||
"", // free
|
||||
std::to_string(self / MiB),
|
||||
std::to_string(mb.model / MiB),
|
||||
std::to_string(mb.context / MiB),
|
||||
std::to_string(mb.compute / MiB),
|
||||
std::to_string(row.model / MiB),
|
||||
std::to_string(row.context / MiB),
|
||||
std::to_string(row.compute / MiB),
|
||||
""}); // unaccounted
|
||||
|
||||
rows.push_back({
|
||||
{"kind", "buffer_type"},
|
||||
{"name", name},
|
||||
json_rows.push_back({
|
||||
{"kind", row.kind == common_memory_breakdown_row::HOST ? "host" : "buffer_type"},
|
||||
{"name", row.name},
|
||||
{"self", self / MiB},
|
||||
{"model", mb.model / MiB},
|
||||
{"context", mb.context / MiB},
|
||||
{"compute", mb.compute / MiB},
|
||||
{"model", row.model / MiB},
|
||||
{"context", row.context / MiB},
|
||||
{"compute", row.compute / MiB},
|
||||
});
|
||||
|
||||
seen_buffer_types.insert(buft);
|
||||
}
|
||||
|
||||
for (size_t j = 1; j < table_data[0].size(); j++) {
|
||||
@@ -1081,7 +1175,7 @@ void common_memory_breakdown_print(const struct llama_context * ctx) {
|
||||
|
||||
LOG_JSON("fit_memory_breakdown", common_json({
|
||||
{"unit", "MiB"},
|
||||
{"rows", rows},
|
||||
{"rows", json_rows},
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "ggml.h"
|
||||
#include "llama.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum common_params_fit_status {
|
||||
@@ -45,6 +46,47 @@ void common_fit_print(
|
||||
|
||||
void common_memory_breakdown_print(const llama_context * ctx);
|
||||
|
||||
// one row of the memory breakdown: a device, the host or another buffer type
|
||||
struct common_memory_breakdown_row {
|
||||
enum kind_t { DEVICE, HOST, BUFFER_TYPE };
|
||||
|
||||
kind_t kind = DEVICE;
|
||||
std::string name; // device or buffer type name, "Host" for the host row
|
||||
std::string description; // device description, empty otherwise
|
||||
int64_t total = 0; // device memory in bytes, 0 unless kind == DEVICE
|
||||
int64_t free = 0; // free device memory in bytes, 0 unless kind == DEVICE
|
||||
size_t model = 0; // bytes held by this context, by kind of buffer
|
||||
size_t context = 0;
|
||||
size_t compute = 0;
|
||||
};
|
||||
|
||||
// memory a model needs at the min and max context --fit may pick
|
||||
struct common_memory_reports {
|
||||
uint32_t n_ctx_train = 0;
|
||||
uint32_t n_ctx_max = 0; // the largest context that fits the free device memory right now
|
||||
uint32_t n_ctx_min = 0; // --fit-ctx (the smallest context --fit may pick), with the placement set in params
|
||||
std::vector<common_memory_breakdown_row> info_max; // breakdown at n_ctx_max
|
||||
std::vector<common_memory_breakdown_row> info_min; // breakdown at n_ctx_min
|
||||
};
|
||||
|
||||
// rows of common_memory_breakdown_print: model devices first, then host
|
||||
std::vector<common_memory_breakdown_row> common_memory_breakdown_rows(const llama_context * ctx);
|
||||
|
||||
// load model + context with no_alloc and return the memory breakdown, params are used as-is
|
||||
// n_ctx_train and n_ctx are optional outputs; throws on failure
|
||||
std::vector<common_memory_breakdown_row> common_fit_measure(
|
||||
const char * path_model,
|
||||
const llama_model_params * mparams,
|
||||
const llama_context_params * cparams,
|
||||
uint32_t * n_ctx_train,
|
||||
uint32_t * n_ctx,
|
||||
ggml_log_level log_level);
|
||||
|
||||
struct common_params;
|
||||
|
||||
// measure min and max without allocating, params may be modified as with --fit; throws on failure
|
||||
common_memory_reports common_measure_memory(common_params & params);
|
||||
|
||||
struct common_device_memory_data {
|
||||
int64_t total;
|
||||
int64_t free;
|
||||
|
||||
+28
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "build-info.h"
|
||||
#include "common.h"
|
||||
#include "file-lock.h"
|
||||
#include "log.h"
|
||||
#include "http.h"
|
||||
#include "json.h"
|
||||
@@ -161,6 +162,14 @@ static bool is_valid_oid(const std::string & oid) {
|
||||
return is_hex_string(oid, 40) || is_hex_string(oid, 64);
|
||||
}
|
||||
|
||||
std::string get_lock_path(const std::string & repo_id, const std::string & oid) {
|
||||
if (!is_valid_repo_id(repo_id) || !is_valid_oid(oid)) {
|
||||
return {};
|
||||
}
|
||||
fs::path lock_path = fs::path(get_cache_path()) / ".locks" / repo_to_folder_name(repo_id) / (oid + ".lock");
|
||||
return lock_path.string();
|
||||
}
|
||||
|
||||
static bool is_valid_subpath(const fs::path & path, const fs::path & subpath) {
|
||||
if (subpath.is_absolute()) {
|
||||
return false; // never do a / b with b absolute
|
||||
@@ -437,7 +446,7 @@ hf_files get_cached_files(const std::string & repo_id) {
|
||||
return files;
|
||||
}
|
||||
|
||||
std::string finalize_file(const hf_file & file) {
|
||||
std::string finalize_file(const hf_file & file, const std::function<bool()> & keep_waiting) {
|
||||
static std::atomic<bool> symlinks_disabled{false};
|
||||
|
||||
std::error_code ec;
|
||||
@@ -452,6 +461,24 @@ std::string finalize_file(const hf_file & file) {
|
||||
return file.final_path;
|
||||
}
|
||||
|
||||
// the lock file is shared with huggingface_hub: another process may be
|
||||
// finalizing the same blob
|
||||
common_file_lock lock(get_lock_path(file.repo_id, file.oid));
|
||||
if (!lock.acquire(keep_waiting)) {
|
||||
// without the lock, another process could move the blob while we check it
|
||||
throw std::runtime_error("failed to lock cache for finalization: " + file.final_path);
|
||||
}
|
||||
|
||||
// acquire() only polls keep_waiting while waiting for a busy lock
|
||||
if (keep_waiting && !keep_waiting()) {
|
||||
throw std::runtime_error("download cancelled before finalization: " + file.final_path);
|
||||
}
|
||||
|
||||
// re-check under the lock: the winner may have created the link already
|
||||
if (fs::exists(final_path, ec)) {
|
||||
return file.final_path;
|
||||
}
|
||||
|
||||
fs::create_directories(final_path.parent_path(), ec);
|
||||
|
||||
if (!symlinks_disabled) {
|
||||
|
||||
+9
-2
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -26,8 +27,14 @@ hf_files get_repo_files(
|
||||
|
||||
hf_files get_cached_files(const std::string & repo_id = {});
|
||||
|
||||
// Create snapshot path (link or move/copy) and return it
|
||||
std::string finalize_file(const hf_file & file);
|
||||
// Lock file path for a blob, shared with the hf CLI: <cache>/.locks/<repo folder>/<oid>.lock
|
||||
// Pure path computation: creates nothing. Returns an empty string on invalid repo or oid.
|
||||
std::string get_lock_path(const std::string & repo_id, const std::string & oid);
|
||||
|
||||
// Create snapshot path (link or move/copy) and return it.
|
||||
// Throws when the lock cannot be taken or keep_waiting returns false.
|
||||
// A completed snapshot (final_path present) is returned without the lock, so a read-only cache works.
|
||||
std::string finalize_file(const hf_file & file, const std::function<bool()> & keep_waiting = {});
|
||||
|
||||
// Remove the entire cached directory for a repo, returns true if removed
|
||||
bool remove_cached_repo(const std::string & repo_id);
|
||||
|
||||
+141
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "log.h"
|
||||
|
||||
#include <cpp-httplib/httplib.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -97,6 +99,118 @@ static common_http_url common_http_parse_url(const std::string & url) {
|
||||
return parts;
|
||||
}
|
||||
|
||||
// match a loopback destination: a remote proxy can never reach the client machine's own
|
||||
// loopback, so these hosts are always contacted directly, before NO_PROXY is consulted
|
||||
// (similar to Go's net/http)
|
||||
static bool common_http_is_loopback(const std::string & host) {
|
||||
auto lower = [](std::string s) {
|
||||
for (auto & c : s) {
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
c += 'a' - 'A';
|
||||
}
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
const std::string h = lower(host);
|
||||
|
||||
if (h == "localhost" || h == "::1" || h == "0.0.0.0") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (h.size() > 10 && h.compare(h.size() - 10, 10, ".localhost") == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv4 literal: exactly four all-digit octets with values 0-255, loopback is 127.0.0.0/8
|
||||
int octets[4] = { 0, 0, 0, 0 };
|
||||
size_t pos = 0;
|
||||
int n = 0;
|
||||
bool complete = false;
|
||||
while (n < 4 && pos <= h.size()) {
|
||||
size_t dot = h.find('.', pos);
|
||||
if (dot == std::string::npos) {
|
||||
dot = h.size();
|
||||
}
|
||||
if (dot == pos || dot - pos > 3) {
|
||||
return false;
|
||||
}
|
||||
int val = 0;
|
||||
for (size_t i = pos; i < dot; i++) {
|
||||
if (h[i] < '0' || h[i] > '9') {
|
||||
return false;
|
||||
}
|
||||
val = val * 10 + (h[i] - '0');
|
||||
}
|
||||
if (val > 255) {
|
||||
return false;
|
||||
}
|
||||
octets[n++] = val;
|
||||
if (dot == h.size()) {
|
||||
complete = (n == 4);
|
||||
break;
|
||||
}
|
||||
pos = dot + 1;
|
||||
}
|
||||
|
||||
return complete && octets[0] == 127;
|
||||
}
|
||||
|
||||
// match a host against a NO_PROXY list: comma separated entries, "*" bypasses the proxy for every host,
|
||||
// an entry matches the host itself or any of its subdomains, a leading dot is optional, ports are ignored
|
||||
static bool common_http_proxy_bypass(const std::string & no_proxy, const std::string & host) {
|
||||
auto lower = [](std::string s) {
|
||||
for (auto & c : s) {
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
c += 'a' - 'A';
|
||||
}
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
const std::string h = lower(host);
|
||||
|
||||
for (size_t pos = 0; pos < no_proxy.size(); ) {
|
||||
size_t end = no_proxy.find(',', pos);
|
||||
if (end == std::string::npos) {
|
||||
end = no_proxy.size();
|
||||
}
|
||||
|
||||
std::string entry = lower(no_proxy.substr(pos, end - pos));
|
||||
pos = end + 1;
|
||||
|
||||
entry.erase(0, entry.find_first_not_of(" \t"));
|
||||
auto last = entry.find_last_not_of(" \t");
|
||||
if (last == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
entry.erase(last + 1);
|
||||
|
||||
if (entry == "*") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.front() == '.') {
|
||||
entry.erase(0, 1);
|
||||
}
|
||||
|
||||
if (entry.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (h == entry) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (h.size() > entry.size() && h[h.size() - entry.size() - 1] == '.' &&
|
||||
h.compare(h.size() - entry.size(), entry.size(), entry) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static std::pair<httplib::Client, common_http_url> common_http_client(const std::string & url) {
|
||||
common_http_url parts = common_http_parse_url(url);
|
||||
|
||||
@@ -123,6 +237,33 @@ static std::pair<httplib::Client, common_http_url> common_http_client(const std:
|
||||
|
||||
cli.set_follow_location(true);
|
||||
|
||||
// Honor HTTP_PROXY / HTTPS_PROXY environment variables (both lower- and upper-case).
|
||||
auto getenv_s = [](const char * upper, const char * lower) -> std::string {
|
||||
const char * val = std::getenv(upper);
|
||||
if (!val) {
|
||||
val = std::getenv(lower);
|
||||
}
|
||||
return val ? val : "";
|
||||
};
|
||||
|
||||
const std::string proxy_url = (parts.scheme == "https")
|
||||
? getenv_s("HTTPS_PROXY", "https_proxy")
|
||||
: getenv_s("HTTP_PROXY", "http_proxy");
|
||||
|
||||
if (!proxy_url.empty() && !common_http_is_loopback(parts.host) &&
|
||||
!common_http_proxy_bypass(getenv_s("NO_PROXY", "no_proxy"), parts.host)) {
|
||||
try {
|
||||
common_http_url proxy = common_http_parse_url(proxy_url);
|
||||
cli.set_proxy(proxy.host, proxy.port);
|
||||
if (!proxy.user.empty()) {
|
||||
cli.set_proxy_basic_auth(proxy.user, proxy.password);
|
||||
}
|
||||
} catch (const std::exception & e) {
|
||||
// fall back to a direct connection, the URL itself is not logged as it can carry credentials
|
||||
LOG_WRN("%s: ignoring malformed proxy URL from the environment: %s\n", __func__, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
return { std::move(cli), std::move(parts) };
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
// we can remove it in the future when we figure out a better way to define inputs using jinja::value
|
||||
#include "json.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
|
||||
#define FILENAME "jinja-caps"
|
||||
@@ -184,6 +186,10 @@ caps caps_get(jinja::program & prog) {
|
||||
{
|
||||
{"role", "user"},
|
||||
{"content", json::array({
|
||||
{
|
||||
{"type", "text"},
|
||||
{"text", content_marker}
|
||||
}
|
||||
})}
|
||||
}
|
||||
});
|
||||
@@ -584,6 +590,58 @@ caps caps_get(jinja::program & prog) {
|
||||
}
|
||||
);
|
||||
|
||||
if (result.supports_reasoning_effort) {
|
||||
JJ_DEBUG("%s\n", ">>> Running capability check: reasoning effort levels");
|
||||
|
||||
static const std::vector<std::string> candidates = { "minimal", "low", "medium", "high", "xhigh", "max" };
|
||||
|
||||
// renders a single user turn with the given effort, nothing when the template raises
|
||||
const auto render = [&](const std::string & effort) {
|
||||
std::optional<std::string> rendered;
|
||||
caps_try_execute(
|
||||
prog,
|
||||
[&]() {
|
||||
// messages
|
||||
return json::array({
|
||||
{
|
||||
{"role", "user"},
|
||||
{"content", "User message"}
|
||||
},
|
||||
});
|
||||
},
|
||||
[&](context & ctx) {
|
||||
ctx.set_val("enable_thinking", mk_val<value_bool>(true));
|
||||
caps_apply_reasoning_effort(ctx, effort);
|
||||
},
|
||||
nullptr, // tools_fn
|
||||
[&](context &, bool success, value &, value &, const std::string & output) {
|
||||
if (success) {
|
||||
rendered = output;
|
||||
}
|
||||
}
|
||||
);
|
||||
return rendered;
|
||||
};
|
||||
|
||||
// an unknown level shows the render of the template fallback, a level that renders the same is not honored
|
||||
// levels sharing a render are aliases and the highest one is kept
|
||||
const auto unknown = render("REASONING_EFFORT_UNKNOWN");
|
||||
std::vector<std::string> renders;
|
||||
for (const auto & level : candidates) {
|
||||
const auto rendered = render(level);
|
||||
if (!rendered || rendered == unknown) {
|
||||
continue;
|
||||
}
|
||||
const auto it = std::find(renders.begin(), renders.end(), *rendered);
|
||||
if (it != renders.end()) {
|
||||
result.reasoning_effort_levels[it - renders.begin()] = level;
|
||||
} else {
|
||||
renders.push_back(*rendered);
|
||||
result.reasoning_effort_levels.push_back(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JJ_DEBUG("%s\n", result.to_string().c_str());
|
||||
|
||||
return result;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace jinja {
|
||||
|
||||
@@ -19,6 +20,9 @@ struct caps {
|
||||
// supports reasoning effort levels
|
||||
bool supports_reasoning_effort = false;
|
||||
|
||||
// reasoning effort levels the template renders distinctly, from lowest to highest
|
||||
std::vector<std::string> reasoning_effort_levels;
|
||||
|
||||
// one of the 2 content capabilities must be true
|
||||
bool supports_string_content = true;
|
||||
bool supports_typed_content = false;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "lexer.h"
|
||||
#include "runtime.h"
|
||||
#include "unicode.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -75,6 +77,24 @@ lexer_result lexer::tokenize(const std::string & source) {
|
||||
}
|
||||
// add escaped char
|
||||
char escaped_char = src[pos++];
|
||||
if (escaped_char == 'x') {
|
||||
if (pos + 1 >= src.size()) {
|
||||
throw lexer_exception("incomplete hexadecimal escape", source, pos);
|
||||
}
|
||||
|
||||
unsigned int value = 0;
|
||||
const char * first = &src[pos];
|
||||
const char * last = first + 2;
|
||||
|
||||
auto [ptr, ec] = std::from_chars(first, last, value, 16);
|
||||
if (ec != std::errc{} || ptr != last) {
|
||||
throw lexer_exception("invalid hexadecimal escape", source, pos);
|
||||
}
|
||||
|
||||
str += common_unicode_cpt_to_utf8(value);
|
||||
pos += 2;
|
||||
continue;
|
||||
}
|
||||
if (escape_chars.find(escaped_char) == escape_chars.end()) {
|
||||
throw lexer_exception(std::string("unknown escape character \\") + escaped_char, source, pos);
|
||||
}
|
||||
|
||||
+21
-3
@@ -21,6 +21,17 @@ static bool is_type(const statement_ptr & ptr) {
|
||||
class parser {
|
||||
const std::vector<token> & tokens;
|
||||
size_t current = 0;
|
||||
int depth = 0;
|
||||
|
||||
struct depth_guard {
|
||||
parser & p;
|
||||
depth_guard(parser & p) : p(p) {
|
||||
if (++p.depth > JINJA_RECURSIVE_LIMIT) {
|
||||
throw parser_exception("Parser Error: template is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep", p.source, p.peek().pos);
|
||||
}
|
||||
}
|
||||
~depth_guard() { p.depth--; }
|
||||
};
|
||||
|
||||
std::string source; // for error reporting
|
||||
|
||||
@@ -94,6 +105,7 @@ private:
|
||||
}
|
||||
|
||||
statement_ptr parse_any() {
|
||||
depth_guard guard(*this);
|
||||
size_t start_pos = current;
|
||||
switch (peek().t) {
|
||||
case token::comment:
|
||||
@@ -254,6 +266,7 @@ private:
|
||||
size_t pos0 = current;
|
||||
++current; // consume {%
|
||||
++current; // consume 'elif'
|
||||
depth_guard guard(*this);
|
||||
alternate.push_back(parse_if_statement(pos0)); // nested If
|
||||
} else if (is_statement({"else"})) {
|
||||
++current; // consume {%
|
||||
@@ -330,6 +343,7 @@ private:
|
||||
}
|
||||
|
||||
statement_ptr parse_if_expression() {
|
||||
depth_guard guard(*this);
|
||||
auto a = parse_logical_or_expression();
|
||||
if (is_identifier("if")) {
|
||||
// Ternary expression
|
||||
@@ -373,6 +387,7 @@ private:
|
||||
statement_ptr parse_logical_negation_expression() {
|
||||
// Try parse unary operators
|
||||
if (is_identifier("not")) {
|
||||
depth_guard guard(*this);
|
||||
size_t start_pos = current;
|
||||
auto op = next();
|
||||
return mk_stmt<unary_expression>(start_pos, op, parse_logical_negation_expression());
|
||||
@@ -458,6 +473,7 @@ private:
|
||||
|
||||
statement_ptr parse_unary_expression() {
|
||||
if (is(token::unary_operator)) {
|
||||
depth_guard guard(*this);
|
||||
size_t start_pos = current;
|
||||
auto op = next();
|
||||
return mk_stmt<unary_expression>(start_pos, op, parse_unary_expression());
|
||||
@@ -477,9 +493,11 @@ private:
|
||||
size_t start_pos = current;
|
||||
auto expr = mk_stmt<call_expression>(start_pos, std::move(callee), parse_args());
|
||||
auto member = parse_member_expression(std::move(expr)); // foo.x().y
|
||||
return is(token::open_paren)
|
||||
? parse_call_expression(std::move(member)) // foo.x()()
|
||||
: std::move(member);
|
||||
if (!is(token::open_paren)) {
|
||||
return member;
|
||||
}
|
||||
depth_guard guard(*this);
|
||||
return parse_call_expression(std::move(member)); // foo.x()()
|
||||
}
|
||||
|
||||
statements parse_args() {
|
||||
|
||||
+27
-10
@@ -3,6 +3,7 @@
|
||||
#include "value.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
@@ -52,32 +53,39 @@ static void ensure_key_type_allowed(const value & val) {
|
||||
|
||||
// execute with error handling
|
||||
value statement::execute(context & ctx) const {
|
||||
std::exception_ptr pending;
|
||||
std::string message;
|
||||
try {
|
||||
return execute_impl(ctx);
|
||||
} catch (const continue_statement::signal & /* ex */) {
|
||||
throw;
|
||||
pending = std::current_exception();
|
||||
} catch (const break_statement::signal & /* ex */) {
|
||||
throw;
|
||||
pending = std::current_exception();
|
||||
} catch (const rethrown_exception & /* ex */) {
|
||||
throw;
|
||||
pending = std::current_exception();
|
||||
} catch (const not_implemented_exception & /* ex */) {
|
||||
throw;
|
||||
pending = std::current_exception();
|
||||
} catch (const std::exception & e) {
|
||||
const std::string & source = *ctx.src;
|
||||
if (source.empty()) {
|
||||
std::ostringstream oss;
|
||||
oss << "\nError executing " << type() << " at position " << pos << ": " << e.what();
|
||||
throw rethrown_exception(oss.str());
|
||||
message = oss.str();
|
||||
} else {
|
||||
std::ostringstream oss;
|
||||
oss << "\n------------\n";
|
||||
oss << "While executing " << type() << " at " << get_line_col(source, pos) << " in source:\n";
|
||||
oss << peak_source(source, pos) << "\n";
|
||||
oss << "Error: " << e.what();
|
||||
// throw as another exception to avoid repeated formatting
|
||||
throw rethrown_exception(oss.str());
|
||||
message = oss.str();
|
||||
}
|
||||
}
|
||||
// throw outside the catch: MSVC keeps the unwound frames on the stack while a catch block runs
|
||||
if (pending) {
|
||||
std::rethrow_exception(pending);
|
||||
}
|
||||
// throw as another exception to avoid repeated formatting
|
||||
throw rethrown_exception(message);
|
||||
}
|
||||
|
||||
value identifier::execute_impl(context & ctx) const {
|
||||
@@ -600,6 +608,7 @@ value for_statement::execute_impl(context & ctx) const {
|
||||
bool noIteration = true;
|
||||
for (size_t i = 0; i < filtered_items.size(); i++) {
|
||||
JJ_DEBUG("For loop iteration %zu/%zu", i + 1, filtered_items.size());
|
||||
context loop_scope(scope);
|
||||
value_object loop_obj = mk_val<value_object>();
|
||||
loop_obj->has_builtins = false; // loop object has no builtins
|
||||
loop_obj->insert("index", mk_val<value_int>(i + 1));
|
||||
@@ -611,11 +620,11 @@ value for_statement::execute_impl(context & ctx) const {
|
||||
loop_obj->insert("length", mk_val<value_int>(filtered_items.size()));
|
||||
loop_obj->insert("previtem", i > 0 ? filtered_items[i - 1] : mk_val<value_undefined>("previtem"));
|
||||
loop_obj->insert("nextitem", i < filtered_items.size() - 1 ? filtered_items[i + 1] : mk_val<value_undefined>("nextitem"));
|
||||
scope.set_val("loop", loop_obj);
|
||||
scope_update_fns[i](scope);
|
||||
loop_scope.set_val("loop", loop_obj);
|
||||
scope_update_fns[i](loop_scope);
|
||||
try {
|
||||
for (auto & stmt : body) {
|
||||
value val = stmt->execute(scope);
|
||||
value val = stmt->execute(loop_scope);
|
||||
result->push_back(val);
|
||||
}
|
||||
} catch (const continue_statement::signal &) {
|
||||
@@ -746,7 +755,11 @@ value macro_statement::execute_impl(context & ctx) const {
|
||||
std::string name = cast_stmt<identifier>(this->name)->val;
|
||||
|
||||
const func_handler func = [this, name](const func_args & args) -> value {
|
||||
if (args.ctx.macro_depth >= JINJA_RECURSIVE_LIMIT) {
|
||||
throw std::runtime_error("macro calls are nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep");
|
||||
}
|
||||
context macro_ctx(args.ctx); // new scope for macro execution
|
||||
macro_ctx.macro_depth = args.ctx.macro_depth + 1;
|
||||
|
||||
bind_parameters(name, this->args, args, macro_ctx);
|
||||
|
||||
@@ -777,7 +790,11 @@ value call_statement::execute_impl(context & ctx) const {
|
||||
context caller_ctx(ctx); // new scope for caller execution
|
||||
|
||||
const func_handler func = [this, caller_ctx = std::move(caller_ctx)](const func_args & args) -> value {
|
||||
if (args.ctx.macro_depth >= JINJA_RECURSIVE_LIMIT) {
|
||||
throw std::runtime_error("macro calls are nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep");
|
||||
}
|
||||
context block_ctx(caller_ctx); // new scope for block execution
|
||||
block_ctx.macro_depth = args.ctx.macro_depth + 1;
|
||||
|
||||
bind_parameters("caller", this->caller_args, args, block_ctx);
|
||||
|
||||
|
||||
+12
-11
@@ -58,6 +58,8 @@ struct context {
|
||||
|
||||
bool is_get_stats = false; // whether to collect stats
|
||||
|
||||
int macro_depth = 0;
|
||||
|
||||
visitor_fn visitor;
|
||||
|
||||
// src is optional, used for error reporting
|
||||
@@ -74,16 +76,13 @@ struct context {
|
||||
}
|
||||
~context() = default;
|
||||
|
||||
context(const context & parent) : context() {
|
||||
context(const context & parent) :
|
||||
src(parent.src),
|
||||
current_time(parent.current_time),
|
||||
is_get_stats(parent.is_get_stats),
|
||||
macro_depth(parent.macro_depth),
|
||||
// inherit variables (for example, when entering a new scope)
|
||||
auto & pvar = parent.env->as_ordered_object();
|
||||
for (const auto & pair : pvar) {
|
||||
set_val(pair.first, pair.second);
|
||||
}
|
||||
current_time = parent.current_time;
|
||||
is_get_stats = parent.is_get_stats;
|
||||
src = parent.src;
|
||||
}
|
||||
env(mk_val<value_object>(*parent.env)) {}
|
||||
|
||||
value get_val(const std::string & name) {
|
||||
value default_val = mk_val<value_undefined>(name);
|
||||
@@ -714,7 +713,7 @@ struct rethrown_exception : public std::exception {
|
||||
|
||||
//////////////////////
|
||||
|
||||
static void gather_string_parts_recursive(const value & val, value_string & parts) {
|
||||
static void gather_string_parts_recursive(const value & val, value_string & parts, int depth = 0) {
|
||||
// TODO: probably allow print value_none as "None" string? currently this breaks some templates
|
||||
if (is_val<value_string>(val)) {
|
||||
const auto & str_val = cast_val<value_string>(val)->val_str;
|
||||
@@ -722,10 +721,12 @@ static void gather_string_parts_recursive(const value & val, value_string & part
|
||||
} else if (is_val<value_int>(val) || is_val<value_float>(val) || is_val<value_bool>(val)) {
|
||||
std::string str_val = val->as_string().str();
|
||||
parts->val_str.append(str_val);
|
||||
} else if (is_val<value_array>(val) && depth >= JINJA_RECURSIVE_LIMIT) {
|
||||
throw std::runtime_error("value is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep");
|
||||
} else if (is_val<value_array>(val)) {
|
||||
auto items = cast_val<value_array>(val)->as_array();
|
||||
for (const auto & item : items) {
|
||||
gather_string_parts_recursive(item, parts);
|
||||
gather_string_parts_recursive(item, parts, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#define JINJA_RECURSIVE_LIMIT 128
|
||||
|
||||
namespace jinja {
|
||||
|
||||
static void string_replace_all(std::string & s, const std::string & search, const std::string & replace) {
|
||||
|
||||
+130
-83
@@ -6,6 +6,7 @@
|
||||
#include "json.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
@@ -244,6 +245,8 @@ static std::string json_ensure_ascii_preserving_format(const std::string & json_
|
||||
return output;
|
||||
}
|
||||
|
||||
static std::string float_repr(double val);
|
||||
|
||||
static value tojson(const func_args & args) {
|
||||
args.ensure_count(1, 5);
|
||||
value val_ascii = args.get_kwarg_or_pos("ensure_ascii", 1);
|
||||
@@ -297,74 +300,53 @@ static value & get_attribute(const value & val, const value & attr, value & defa
|
||||
return default_val;
|
||||
}
|
||||
|
||||
template<bool is_reject>
|
||||
static value selectattr(const func_args & args) {
|
||||
args.ensure_count(2, 4);
|
||||
args.ensure_vals<value_array, value_string, value_string, value_string>(true, true, false, false);
|
||||
template<bool is_reject, bool lookup_attr>
|
||||
static value select_or_reject(const func_args & args) {
|
||||
// example: array | select("odd") -> test_is_odd(item)
|
||||
// example: array | selectattr("status", "equalto", "active") -> test_is_equalto(item.status, "active")
|
||||
constexpr size_t test_pos = lookup_attr ? 2 : 1;
|
||||
args.ensure_count(test_pos);
|
||||
args.ensure_val<value_array>(args.get_pos(0));
|
||||
value attr;
|
||||
if constexpr (lookup_attr) {
|
||||
attr = args.get_pos(1);
|
||||
args.ensure_val<value_string>(attr);
|
||||
}
|
||||
|
||||
auto & builtins = global_builtins();
|
||||
auto test_it = builtins.end();
|
||||
if (args.count() > test_pos) {
|
||||
args.ensure_val<value_string>(args.get_pos(test_pos));
|
||||
std::string test_name = args.get_pos(test_pos)->as_string().str();
|
||||
test_it = builtins.find("test_is_" + test_name);
|
||||
if (test_it == builtins.end()) {
|
||||
throw raised_exception(args.func_name + ": unknown test '" + test_name + "'");
|
||||
}
|
||||
}
|
||||
|
||||
auto arr = args.get_pos(0)->as_array();
|
||||
auto attribute = args.get_pos(1);
|
||||
auto out = mk_val<value_array>();
|
||||
value val_default = mk_val<value_undefined>();
|
||||
|
||||
if (args.count() == 2) {
|
||||
// example: array | selectattr("active")
|
||||
for (const auto & item : arr) {
|
||||
value attr_val = get_attribute(item, attribute, val_default);
|
||||
bool is_selected = attr_val->as_bool();
|
||||
if constexpr (is_reject) is_selected = !is_selected;
|
||||
if (is_selected) out->push_back(item);
|
||||
for (const auto & item : arr) {
|
||||
value val = item;
|
||||
if constexpr (lookup_attr) {
|
||||
val = get_attribute(item, attr, val_default);
|
||||
}
|
||||
return out;
|
||||
|
||||
} else if (args.count() == 3) {
|
||||
// example: array | selectattr("equalto", "text")
|
||||
// translated to: test_is_equalto(item, "text")
|
||||
std::string test_name = args.get_pos(1)->as_string().str();
|
||||
value test_val = args.get_pos(2);
|
||||
auto & builtins = global_builtins();
|
||||
auto it = builtins.find("test_is_" + test_name);
|
||||
if (it == builtins.end()) {
|
||||
throw raised_exception("selectattr: unknown test '" + test_name + "'");
|
||||
}
|
||||
auto test_fn = it->second;
|
||||
for (const auto & item : arr) {
|
||||
bool is_selected;
|
||||
if (test_it == builtins.end()) {
|
||||
is_selected = val->as_bool();
|
||||
} else {
|
||||
func_args test_args(args.ctx);
|
||||
test_args.push_back(item); // current object
|
||||
test_args.push_back(test_val); // extra argument
|
||||
value test_result = test_fn(test_args);
|
||||
bool is_selected = test_result->as_bool();
|
||||
if constexpr (is_reject) is_selected = !is_selected;
|
||||
if (is_selected) out->push_back(item);
|
||||
test_args.push_back(val);
|
||||
for (size_t i = test_pos + 1; i < args.count(); i++) {
|
||||
test_args.push_back(args.get_pos(i));
|
||||
}
|
||||
is_selected = test_it->second(test_args)->as_bool();
|
||||
}
|
||||
return out;
|
||||
|
||||
} else if (args.count() == 4) {
|
||||
// example: array | selectattr("status", "equalto", "active")
|
||||
// translated to: test_is_equalto(item.status, "active")
|
||||
std::string test_name = args.get_pos(2)->as_string().str();
|
||||
auto extra_arg = args.get_pos(3);
|
||||
auto & builtins = global_builtins();
|
||||
auto it = builtins.find("test_is_" + test_name);
|
||||
if (it == builtins.end()) {
|
||||
throw raised_exception("selectattr: unknown test '" + test_name + "'");
|
||||
}
|
||||
auto test_fn = it->second;
|
||||
for (const auto & item : arr) {
|
||||
value attr_val = get_attribute(item, attribute, val_default);
|
||||
func_args test_args(args.ctx);
|
||||
test_args.push_back(attr_val); // attribute value
|
||||
test_args.push_back(extra_arg); // extra argument
|
||||
value test_result = test_fn(test_args);
|
||||
bool is_selected = test_result->as_bool();
|
||||
if constexpr (is_reject) is_selected = !is_selected;
|
||||
if (is_selected) out->push_back(item);
|
||||
}
|
||||
return out;
|
||||
} else {
|
||||
throw raised_exception("selectattr: invalid number of arguments");
|
||||
if constexpr (is_reject) is_selected = !is_selected;
|
||||
if (is_selected) out->push_back(item);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -423,7 +405,12 @@ const func_builtins & global_builtins() {
|
||||
throw raised_exception("Jinja Exception: " + msg);
|
||||
}},
|
||||
{"dict", toobject},
|
||||
{"namespace", toobject},
|
||||
{"namespace", [](const func_args & args) -> value {
|
||||
// unlike a dict, a namespace has no methods: ns.items is the attribute, as in jinja2
|
||||
value out = toobject(args);
|
||||
cast_val<value_object>(out)->has_builtins = false;
|
||||
return out;
|
||||
}},
|
||||
{"strftime_now", [](const func_args & args) -> value {
|
||||
args.ensure_vals<value_string>();
|
||||
std::string format = args.get_pos(0)->as_string().str();
|
||||
@@ -634,6 +621,11 @@ const func_builtins & value_int_t::get_builtins() const {
|
||||
|
||||
|
||||
const func_builtins & value_float_t::get_builtins() const {
|
||||
// Python's str() of a float, nan and inf stay as they are (tojson writes NaN and Infinity)
|
||||
static const func_handler tostring = [](const func_args & args) -> value {
|
||||
args.ensure_vals<value_float>();
|
||||
return mk_val<value_string>(float_repr(args.get_pos(0)->as_float()));
|
||||
};
|
||||
static const func_builtins builtins = {
|
||||
{"default", default_value},
|
||||
{"abs", [](const func_args & args) -> value {
|
||||
@@ -650,8 +642,8 @@ const func_builtins & value_float_t::get_builtins() const {
|
||||
args.ensure_vals<value_float>();
|
||||
return mk_val<value_float>(args.get_pos(0)->as_float());
|
||||
}},
|
||||
{"safe", tojson},
|
||||
{"string", tojson},
|
||||
{"safe", tostring},
|
||||
{"string", tostring},
|
||||
{"tojson", tojson},
|
||||
};
|
||||
return builtins;
|
||||
@@ -1021,10 +1013,6 @@ const func_builtins & value_bool_t::get_builtins() const {
|
||||
return builtins;
|
||||
}
|
||||
|
||||
[[noreturn]] static value array_unique_not_implemented(const func_args &) {
|
||||
throw not_implemented_exception("Array unique builtin not implemented");
|
||||
}
|
||||
|
||||
const func_builtins & value_array_t::get_builtins() const {
|
||||
static const func_builtins builtins = {
|
||||
{"default", default_value},
|
||||
@@ -1087,10 +1075,10 @@ const func_builtins & value_array_t::get_builtins() const {
|
||||
auto arr = slice(val->as_array(), start, stop, step);
|
||||
return is_val<value_tuple>(val) ? mk_val<value_tuple>(std::move(arr)) : mk_val<value_array>(std::move(arr));
|
||||
}},
|
||||
{"selectattr", selectattr<false>},
|
||||
{"select", selectattr<false>},
|
||||
{"rejectattr", selectattr<true>},
|
||||
{"reject", selectattr<true>},
|
||||
{"selectattr", select_or_reject<false, true>},
|
||||
{"select", select_or_reject<false, false>},
|
||||
{"rejectattr", select_or_reject<true, true>},
|
||||
{"reject", select_or_reject<true, false>},
|
||||
{"join", [](const func_args & args) -> value {
|
||||
args.ensure_count(1, 3);
|
||||
if (!is_val<value_array>(args.get_pos(0))) {
|
||||
@@ -1250,7 +1238,36 @@ const func_builtins & value_array_t::get_builtins() const {
|
||||
}
|
||||
return result;
|
||||
}},
|
||||
{"unique", array_unique_not_implemented},
|
||||
{"unique", [](const func_args & args) -> value {
|
||||
args.ensure_count(1, 3);
|
||||
args.ensure_vals<value_array>();
|
||||
value val = args.get_pos(0);
|
||||
value val_case = args.get_kwarg_or_pos("case_sensitive", 1);
|
||||
value attribute = args.get_kwarg_or_pos("attribute", 2);
|
||||
// FIXME: unique is currently always case sensitive
|
||||
//const bool case_sensitive = val_case->as_bool(); // undefined == false
|
||||
value undef = mk_val<value_undefined>();
|
||||
std::vector<value> result;
|
||||
std::vector<value> seen;
|
||||
for (const auto & item : val->as_array()) {
|
||||
value key = item;
|
||||
if (!attribute->is_undefined()) {
|
||||
key = get_attribute(item, attribute, undef);
|
||||
}
|
||||
bool dup = false;
|
||||
for (const auto & s : seen) {
|
||||
if (value_compare(s, key, value_compare_op::eq)) {
|
||||
dup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!dup) {
|
||||
seen.push_back(key);
|
||||
result.push_back(item);
|
||||
}
|
||||
}
|
||||
return mk_val<value_array>(std::move(result));
|
||||
}},
|
||||
};
|
||||
return builtins;
|
||||
}
|
||||
@@ -1292,6 +1309,15 @@ const func_builtins & value_object_t::get_builtins() const {
|
||||
}
|
||||
return result;
|
||||
}},
|
||||
{"list", [](const func_args & args) -> value {
|
||||
args.ensure_vals<value_object>();
|
||||
const auto & obj = args.get_pos(0)->as_ordered_object();
|
||||
auto result = mk_val<value_array>();
|
||||
for (const auto & pair : obj) {
|
||||
result->push_back(pair.first);
|
||||
}
|
||||
return result;
|
||||
}},
|
||||
{"values", [](const func_args & args) -> value {
|
||||
args.ensure_vals<value_object>();
|
||||
const auto & obj = args.get_pos(0)->as_ordered_object();
|
||||
@@ -1414,7 +1440,8 @@ const func_builtins & value_undefined_t::get_builtins() const {
|
||||
//////////////////////////////////
|
||||
|
||||
|
||||
static value from_json(const common_json & j, bool mark_input) {
|
||||
// the runtime walks values with recursion, a deeper input overflows the stack
|
||||
static value from_json(const common_json & j, bool mark_input, int depth = 0) {
|
||||
if (j.is_null()) {
|
||||
return mk_val<value_none>();
|
||||
} else if (j.is_boolean()) {
|
||||
@@ -1429,16 +1456,18 @@ static value from_json(const common_json & j, bool mark_input) {
|
||||
str->mark_input();
|
||||
}
|
||||
return str;
|
||||
} else if ((j.is_array() || j.is_object()) && depth >= JINJA_RECURSIVE_LIMIT) {
|
||||
throw std::invalid_argument("JSON is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep");
|
||||
} else if (j.is_array()) {
|
||||
auto arr = mk_val<value_array>();
|
||||
for (const auto & item : j) {
|
||||
arr->push_back(from_json(item, mark_input));
|
||||
arr->push_back(from_json(item, mark_input, depth + 1));
|
||||
}
|
||||
return arr;
|
||||
} else if (j.is_object()) {
|
||||
auto obj = mk_val<value_object>();
|
||||
for (auto it = j.begin(); it != j.end(); ++it) {
|
||||
obj->insert(it.key(), from_json(it.value(), mark_input));
|
||||
obj->insert(it.key(), from_json(it.value(), mark_input, depth + 1));
|
||||
}
|
||||
return obj;
|
||||
} else {
|
||||
@@ -1522,10 +1551,10 @@ void global_from_json(context & ctx, const common_json & json_obj, bool mark_inp
|
||||
}
|
||||
}
|
||||
|
||||
// a float as Python writes it (repr, json.dumps): the shortest digits that read back as the same value
|
||||
static std::string float_to_json(double val) {
|
||||
// a float as Python's repr() and str() write it: the shortest digits that read back as the same value
|
||||
static std::string float_repr(double val) {
|
||||
if (!std::isfinite(val)) {
|
||||
return std::isnan(val) ? "nan" : val < 0 ? "-inf" : "inf"; // as before (json.dumps writes NaN and Infinity, str() writes nan and inf)
|
||||
return std::isnan(val) ? "nan" : val < 0 ? "-inf" : "inf";
|
||||
}
|
||||
const double mag = std::fabs(val);
|
||||
|
||||
@@ -1551,6 +1580,7 @@ static std::string float_to_json(double val) {
|
||||
if (prec == 16 || reads_back(d)) {
|
||||
digits = d;
|
||||
} else if (is_pow2 && d.back() != '9') {
|
||||
// a last digit 9 is not bumped (it would carry): no power of 2, from 2^-1074 to 2^1023, needs it
|
||||
d.back()++;
|
||||
if (reads_back(d)) {
|
||||
digits = d;
|
||||
@@ -1581,6 +1611,17 @@ static std::string float_to_json(double val) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// a float as json.dumps writes it: nan and inf as NaN and Infinity, which are not valid JSON
|
||||
static std::string float_json(double val) {
|
||||
if (std::isnan(val)) {
|
||||
return "NaN";
|
||||
}
|
||||
if (std::isinf(val)) {
|
||||
return val < 0 ? "-Infinity" : "Infinity";
|
||||
}
|
||||
return float_repr(val);
|
||||
}
|
||||
|
||||
// recursively convert value to JSON string
|
||||
// TODO: avoid circular references
|
||||
static void value_to_json_internal(std::ostringstream & oss, const value & val, int curr_lvl, int indent, const std::string_view item_sep, const std::string_view key_sep) {
|
||||
@@ -1598,7 +1639,7 @@ static void value_to_json_internal(std::ostringstream & oss, const value & val,
|
||||
} else if (is_val<value_int>(val)) {
|
||||
oss << val->as_int();
|
||||
} else if (is_val<value_float>(val)) {
|
||||
oss << float_to_json(val->as_float());
|
||||
oss << float_json(val->as_float());
|
||||
} else if (is_val<value_string>(val)) {
|
||||
oss << "\"";
|
||||
for (char c : val->as_string().str()) {
|
||||
@@ -1621,6 +1662,8 @@ static void value_to_json_internal(std::ostringstream & oss, const value & val,
|
||||
}
|
||||
}
|
||||
oss << "\"";
|
||||
} else if ((is_val<value_array>(val) || is_val<value_object>(val)) && curr_lvl >= JINJA_RECURSIVE_LIMIT) {
|
||||
throw std::runtime_error("value is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep");
|
||||
} else if (is_val<value_array>(val)) {
|
||||
const auto & arr = val->as_array();
|
||||
oss << "[";
|
||||
@@ -1645,7 +1688,8 @@ static void value_to_json_internal(std::ostringstream & oss, const value & val,
|
||||
size_t i = 0;
|
||||
for (const auto & pair : obj) {
|
||||
oss << indent_str() << (indent > 0 ? std::string(indent, ' ') : "");
|
||||
value_to_json_internal(oss, mk_val<value_string>(pair.first->as_string().str()), curr_lvl + 1, indent, item_sep, key_sep);
|
||||
const std::string key = is_val<value_float>(pair.first) ? float_json(pair.first->as_float()) : pair.first->as_string().str();
|
||||
value_to_json_internal(oss, mk_val<value_string>(key), curr_lvl + 1, indent, item_sep, key_sep);
|
||||
oss << key_sep;
|
||||
value_to_json_internal(oss, pair.second, curr_lvl + 1, indent, item_sep, key_sep);
|
||||
if (i < obj.size() - 1) {
|
||||
@@ -1685,17 +1729,20 @@ std::string value_to_string_repr(const value & val) {
|
||||
}
|
||||
|
||||
// stats utility
|
||||
void value_t::stats_t::mark_used(value & val, bool deep) {
|
||||
void value_t::stats_t::mark_used(value & val, bool deep, int depth) {
|
||||
val->stats.used = true;
|
||||
if (deep && (is_val<value_array>(val) || is_val<value_object>(val)) && depth >= JINJA_RECURSIVE_LIMIT) {
|
||||
throw std::runtime_error("value is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep");
|
||||
}
|
||||
if (deep) {
|
||||
if (is_val<value_array>(val)) {
|
||||
for (auto & item : val->val_arr) {
|
||||
mark_used(item, deep);
|
||||
mark_used(item, deep, depth + 1);
|
||||
}
|
||||
} else if (is_val<value_object>(val)) {
|
||||
for (auto & pair : val->val_obj) {
|
||||
mark_used(pair.first, deep);
|
||||
mark_used(pair.second, deep);
|
||||
mark_used(pair.first, deep, depth + 1);
|
||||
mark_used(pair.second, deep, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ struct value_t {
|
||||
// ops can be builtin calls or operators: "array_access", "object_access"
|
||||
std::set<std::string> ops;
|
||||
// utility to recursively mark value and its children as used
|
||||
static void mark_used(value & val, bool deep = false);
|
||||
static void mark_used(value & val, bool deep = false, int depth = 0);
|
||||
} stats;
|
||||
|
||||
value_t() = default;
|
||||
|
||||
@@ -272,15 +272,12 @@ static bool is_reserved_name(const std::string & name) {
|
||||
return RESERVED_NAMES.find(name) != RESERVED_NAMES.end();
|
||||
}
|
||||
|
||||
static std::regex INVALID_RULE_CHARS_RE("[^a-zA-Z0-9-]+");
|
||||
static std::regex GRAMMAR_LITERAL_ESCAPE_RE("[\r\n\"\\\\]");
|
||||
static std::regex GRAMMAR_RANGE_LITERAL_ESCAPE_RE("[\r\n\"\\]\\-\\\\]");
|
||||
static std::unordered_map<char, std::string> GRAMMAR_LITERAL_ESCAPES = {
|
||||
{'\r', "\\r"}, {'\n', "\\n"}, {'"', "\\\""}, {'-', "\\-"}, {']', "\\]"}, {'\\', "\\\\"}
|
||||
};
|
||||
|
||||
static const int MAX_PATTERN_DEPTH = 100;
|
||||
|
||||
static std::unordered_set<char> NON_LITERAL_SET = {'|', '.', '(', ')', '[', ']', '{', '}', '*', '+', '?', '^', '$'};
|
||||
static std::unordered_set<char> ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = {'^', '$', '.', '[', ']', '(', ')', '|', '{', '}', '*', '+', '?'};
|
||||
|
||||
@@ -312,6 +309,22 @@ static std::string format_literal(const std::string & literal) {
|
||||
|
||||
std::string gbnf_format_literal(const std::string & literal) { return format_literal(literal); }
|
||||
|
||||
// same as std::regex_replace with [^a-zA-Z0-9-]+ -> "-", no std::regex: libstdc++ recurses per char
|
||||
std::string gbnf_format_rule_name(const std::string & name) {
|
||||
std::string out;
|
||||
bool in_invalid = false;
|
||||
for (char c : name) {
|
||||
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-') {
|
||||
out += c;
|
||||
in_invalid = false;
|
||||
} else if (!in_invalid) {
|
||||
out += '-';
|
||||
in_invalid = true;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static size_t gbnf_escape_length(const std::string & pattern, size_t pos) {
|
||||
if (pos + 1 >= pattern.length() || pattern[pos] != '\\') {
|
||||
return 0;
|
||||
@@ -347,6 +360,17 @@ private:
|
||||
std::unordered_set<std::string> _refs_being_resolved;
|
||||
std::vector<std::string> _errors;
|
||||
std::vector<std::string> _warnings;
|
||||
int _depth = 0;
|
||||
|
||||
struct depth_guard {
|
||||
int & depth;
|
||||
depth_guard(int & depth) : depth(depth) {
|
||||
if (++depth > COMMON_RECURSIVE_LIMIT) {
|
||||
throw std::invalid_argument("JSON schema is nested more than " + std::to_string(COMMON_RECURSIVE_LIMIT) + " levels deep");
|
||||
}
|
||||
}
|
||||
~depth_guard() { depth--; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
static const T & as(const common_chat_schema & node) {
|
||||
@@ -354,7 +378,7 @@ private:
|
||||
}
|
||||
|
||||
std::string _add_rule(const std::string & name, const std::string & rule) {
|
||||
std::string esc_name = regex_replace(name, INVALID_RULE_CHARS_RE, "-");
|
||||
std::string esc_name = gbnf_format_rule_name(name);
|
||||
if (_rules.find(esc_name) == _rules.end() || _rules[esc_name] == rule) {
|
||||
_rules[esc_name] = rule;
|
||||
return esc_name;
|
||||
@@ -482,7 +506,7 @@ private:
|
||||
}
|
||||
}
|
||||
paren_depth++;
|
||||
if (paren_depth > MAX_PATTERN_DEPTH) {
|
||||
if (paren_depth > COMMON_RECURSIVE_LIMIT) {
|
||||
throw unsupported_pattern("pattern nesting too deep");
|
||||
}
|
||||
seq.emplace_back("(" + to_rule(transform()) + ")", false);
|
||||
@@ -644,7 +668,11 @@ private:
|
||||
std::string char_rule = _add_primitive("char", PRIMITIVE_RULES.at("char"));
|
||||
std::ostringstream out;
|
||||
out << "[\"] ( ";
|
||||
std::function<void(size_t)> visit = [&](size_t idx) {
|
||||
// one level per char of the longest name
|
||||
std::function<void(size_t, int)> visit = [&](size_t idx, int depth) {
|
||||
if (depth >= COMMON_RECURSIVE_LIMIT) {
|
||||
throw std::invalid_argument("JSON schema property name is longer than " + std::to_string(COMMON_RECURSIVE_LIMIT) + " chars");
|
||||
}
|
||||
const auto & node = trie.nodes[idx];
|
||||
std::string rejects;
|
||||
auto first = true;
|
||||
@@ -659,7 +687,7 @@ private:
|
||||
out << "[" << c << "]";
|
||||
if (!trie.nodes[child].children.empty()) {
|
||||
out << " (";
|
||||
visit(child);
|
||||
visit(child, depth + 1);
|
||||
out << ")";
|
||||
} else {
|
||||
out << " " << char_rule << "+";
|
||||
@@ -669,7 +697,7 @@ private:
|
||||
out << " | [^\"" << rejects << "] " << char_rule << "*";
|
||||
}
|
||||
};
|
||||
visit(0);
|
||||
visit(0, 0);
|
||||
|
||||
out << " )";
|
||||
if (trie.nodes[0].pattern < 0) {
|
||||
@@ -682,8 +710,7 @@ private:
|
||||
std::string _resolve_ref(const common_chat_schema_ref & schema) {
|
||||
auto it = schema.ref.find('#');
|
||||
std::string ref_fragment = it != std::string::npos ? schema.ref.substr(it + 1) : schema.ref;
|
||||
static const std::regex nonalphanumeric_regex(R"([^a-zA-Z0-9-]+)");
|
||||
std::string ref_name = "ref" + std::regex_replace(ref_fragment, nonalphanumeric_regex, "-");
|
||||
std::string ref_name = "ref" + gbnf_format_rule_name(ref_fragment);
|
||||
if (_rules.find(ref_name) == _rules.end() && _refs_being_resolved.find(schema.ref) == _refs_being_resolved.end()) {
|
||||
if (!schema.target) {
|
||||
_errors.push_back("Unresolved $ref " + schema.ref);
|
||||
@@ -833,10 +860,13 @@ public:
|
||||
std::unordered_set<std::string> required;
|
||||
std::vector<std::pair<std::string, const common_chat_schema *>> properties;
|
||||
std::map<std::string, size_t> enum_values;
|
||||
std::function<void(const common_chat_schema &, bool)> add_component = [&](const common_chat_schema & comp, bool is_required) {
|
||||
std::function<void(const common_chat_schema &, bool, int)> add_component = [&](const common_chat_schema & comp, bool is_required, int depth) {
|
||||
if (depth >= COMMON_RECURSIVE_LIMIT) {
|
||||
throw std::invalid_argument("JSON schema is nested more than " + std::to_string(COMMON_RECURSIVE_LIMIT) + " levels deep");
|
||||
}
|
||||
if (comp.kind() == common_chat_schema::KIND_REF) {
|
||||
if (const auto * target = as<common_chat_schema_ref>(comp).target) {
|
||||
add_component(*target, is_required);
|
||||
add_component(*target, is_required, depth + 1);
|
||||
}
|
||||
} else if (comp.kind() == common_chat_schema::KIND_OBJECT) {
|
||||
for (const auto & prop : as<common_chat_schema_object>(comp).properties) {
|
||||
@@ -854,10 +884,10 @@ public:
|
||||
for (const auto & child : schema.children) {
|
||||
if (child->kind() == common_chat_schema::KIND_ANY_OF) {
|
||||
for (const auto & alt : as<common_chat_schema_any_of>(*child).children) {
|
||||
add_component(*alt, false);
|
||||
add_component(*alt, false, 0);
|
||||
}
|
||||
} else {
|
||||
add_component(*child, true);
|
||||
add_component(*child, true, 0);
|
||||
}
|
||||
}
|
||||
if (!enum_values.empty()) {
|
||||
@@ -875,6 +905,8 @@ public:
|
||||
}
|
||||
|
||||
std::string visit(const common_chat_schema & schema, const std::string & name) {
|
||||
// the $ref targets are visited too, a $ref chain is not bounded by the builder
|
||||
depth_guard guard(_depth);
|
||||
std::string rule_name = is_reserved_name(name) ? name + "-" : name.empty() ? "root" : name;
|
||||
std::string sub_name = name + (name.empty() ? "" : "-");
|
||||
|
||||
|
||||
@@ -19,5 +19,6 @@ struct common_grammar_options {
|
||||
};
|
||||
|
||||
std::string gbnf_format_literal(const std::string & literal);
|
||||
std::string gbnf_format_rule_name(const std::string & name);
|
||||
|
||||
std::string build_grammar(const std::function<void(const common_grammar_builder &)> & cb, const common_grammar_options & options = {});
|
||||
|
||||
+34
-10
@@ -9,6 +9,18 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
struct depth_guard {
|
||||
int & depth;
|
||||
depth_guard(int & depth) : depth(depth) {
|
||||
if (++depth > COMMON_RECURSIVE_LIMIT) {
|
||||
throw std::invalid_argument("JSON schema is nested more than " + std::to_string(COMMON_RECURSIVE_LIMIT) + " levels deep");
|
||||
}
|
||||
}
|
||||
~depth_guard() { depth--; }
|
||||
};
|
||||
} // namespace
|
||||
|
||||
class common_chat_schema_builder {
|
||||
const common_json & root_;
|
||||
common_chat_schema_document & doc_;
|
||||
@@ -224,7 +236,14 @@ class common_chat_schema_builder {
|
||||
return node;
|
||||
}
|
||||
|
||||
int depth_ = 0;
|
||||
|
||||
common_chat_schema_ptr build_node(const common_json & schema, const std::string & path) {
|
||||
depth_guard guard(depth_);
|
||||
return build_node_impl(schema, path);
|
||||
}
|
||||
|
||||
common_chat_schema_ptr build_node_impl(const common_json & schema, const std::string & path) {
|
||||
if (!schema.is_object()) {
|
||||
fail(path, "schema must be an object");
|
||||
}
|
||||
@@ -365,7 +384,9 @@ static common_chat_schema::value_type json_type(const common_json & value) {
|
||||
return common_chat_schema::TYPE_OBJECT;
|
||||
}
|
||||
|
||||
static common_chat_schema::type_set value_types_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited) {
|
||||
// depth counts the $ref, anyOf and allOf hops, a $ref chain is not bounded by the builder
|
||||
static common_chat_schema::type_set value_types_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited, int & depth) {
|
||||
depth_guard guard(depth);
|
||||
switch (s.kind()) {
|
||||
case common_chat_schema::KIND_ANY:
|
||||
return common_chat_schema::type_set::all();
|
||||
@@ -399,21 +420,21 @@ static common_chat_schema::type_set value_types_impl(const common_chat_schema &
|
||||
// a cycle contributes no type, to be safe
|
||||
return {};
|
||||
}
|
||||
auto types = value_types_impl(*target, visited);
|
||||
auto types = value_types_impl(*target, visited, depth);
|
||||
visited.erase(target);
|
||||
return types;
|
||||
}
|
||||
case common_chat_schema::KIND_ANY_OF: {
|
||||
common_chat_schema::type_set types;
|
||||
for (const auto & child : static_cast<const common_chat_schema_any_of &>(s).children) {
|
||||
types |= value_types_impl(*child, visited);
|
||||
types |= value_types_impl(*child, visited, depth);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
case common_chat_schema::KIND_ALL_OF: {
|
||||
auto types = common_chat_schema::type_set::all();
|
||||
for (const auto & child : static_cast<const common_chat_schema_all_of &>(s).children) {
|
||||
types &= value_types_impl(*child, visited);
|
||||
types &= value_types_impl(*child, visited, depth);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
@@ -423,10 +444,12 @@ static common_chat_schema::type_set value_types_impl(const common_chat_schema &
|
||||
|
||||
common_chat_schema::type_set common_chat_schema::value_types() const {
|
||||
std::unordered_set<const common_chat_schema *> visited;
|
||||
return value_types_impl(*this, visited);
|
||||
int depth = 0;
|
||||
return value_types_impl(*this, visited, depth);
|
||||
}
|
||||
|
||||
static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited) {
|
||||
static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited, int & depth) {
|
||||
depth_guard guard(depth);
|
||||
switch (s.kind()) {
|
||||
case common_chat_schema::KIND_STRING:
|
||||
return true;
|
||||
@@ -445,13 +468,13 @@ static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set<
|
||||
if (!target || !visited.insert(target).second) {
|
||||
return false;
|
||||
}
|
||||
bool result = may_be_string_impl(*target, visited);
|
||||
bool result = may_be_string_impl(*target, visited, depth);
|
||||
visited.erase(target);
|
||||
return result;
|
||||
}
|
||||
case common_chat_schema::KIND_ANY_OF:
|
||||
for (const auto & child : static_cast<const common_chat_schema_any_of &>(s).children) {
|
||||
if (may_be_string_impl(*child, visited)) {
|
||||
if (may_be_string_impl(*child, visited, depth)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -463,7 +486,7 @@ static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set<
|
||||
if (child->kind() == common_chat_schema::KIND_ANY) {
|
||||
continue;
|
||||
}
|
||||
if (!may_be_string_impl(*child, visited)) {
|
||||
if (!may_be_string_impl(*child, visited, depth)) {
|
||||
return false;
|
||||
}
|
||||
any_string = true;
|
||||
@@ -477,7 +500,8 @@ static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set<
|
||||
|
||||
bool common_chat_schema::may_be_string() const {
|
||||
std::unordered_set<const common_chat_schema *> visited;
|
||||
return may_be_string_impl(*this, visited);
|
||||
int depth = 0;
|
||||
return may_be_string_impl(*this, visited, depth);
|
||||
}
|
||||
|
||||
const char * common_chat_schema::kind_name(node_kind kind) {
|
||||
|
||||
@@ -68,7 +68,7 @@ static llama_token try_draft(common_ngram_cache & nc_static, const common_ngram
|
||||
if (part_static_it == nc_static.end()) {
|
||||
return LLAMA_TOKEN_NULL;
|
||||
}
|
||||
const common_ngram_cache_part part_static = part_static_it->second;
|
||||
const common_ngram_cache_part & part_static = part_static_it->second;
|
||||
|
||||
int max_count_static = 0;
|
||||
int sum_count_static = 0;
|
||||
@@ -96,7 +96,7 @@ static llama_token try_draft(common_ngram_cache & nc_static, const common_ngram
|
||||
|
||||
// Try to draft a token from primary cache (context/dynamic), validate with static cache:
|
||||
static llama_token try_draft(
|
||||
common_ngram_cache & nc_primary, const std::vector<common_ngram> & ngrams_primary, common_ngram_cache_part & part_static,
|
||||
common_ngram_cache & nc_primary, const std::vector<common_ngram> & ngrams_primary, const common_ngram_cache_part & part_static,
|
||||
const int * min_sample_size, const int * min_percent) {
|
||||
|
||||
llama_token drafted_token = LLAMA_TOKEN_NULL;
|
||||
@@ -108,7 +108,7 @@ static llama_token try_draft(
|
||||
if (part_primary_it == nc_primary.end()) {
|
||||
continue;
|
||||
}
|
||||
const common_ngram_cache_part part_primary = part_primary_it->second;
|
||||
const common_ngram_cache_part & part_primary = part_primary_it->second;
|
||||
|
||||
int max_count_primary = 0;
|
||||
int max_count_static = 0;
|
||||
@@ -118,7 +118,7 @@ static llama_token try_draft(
|
||||
for (std::pair<llama_token, int> token_count_primary : part_primary) {
|
||||
const llama_token token = token_count_primary.first;
|
||||
|
||||
common_ngram_cache_part::iterator token_count_static_it = part_static.find(token);
|
||||
common_ngram_cache_part::const_iterator token_count_static_it = part_static.find(token);
|
||||
|
||||
const int32_t count_primary = token_count_primary.second;
|
||||
const int32_t count_static = token_count_static_it != part_static.end() ? 100*token_count_static_it->second : 1;
|
||||
@@ -162,11 +162,9 @@ void common_ngram_cache_draft(
|
||||
for (int j = ngram_start_static; j < ngram_start_static + LLAMA_NGRAM_STATIC; ++j) {
|
||||
ngram_static.tokens[j-ngram_start_static] = get_token(inp, draft, j);
|
||||
}
|
||||
common_ngram_cache::iterator part_static_it = nc_static.find(ngram_static);
|
||||
common_ngram_cache_part part_static;
|
||||
if (part_static_it != nc_static.end()) {
|
||||
part_static = part_static_it->second;
|
||||
}
|
||||
static const common_ngram_cache_part part_static_empty;
|
||||
common_ngram_cache::const_iterator part_static_it = nc_static.find(ngram_static);
|
||||
const common_ngram_cache_part & part_static = part_static_it != nc_static.end() ? part_static_it->second : part_static_empty;
|
||||
|
||||
// cd = context + dynamic
|
||||
std::vector<common_ngram> ngrams_cd;
|
||||
|
||||
+53
-10
@@ -1,4 +1,5 @@
|
||||
#include "parsers.h"
|
||||
#include "log.h"
|
||||
|
||||
// The DeepSeek V4 reference implementation renders consecutive tool results into a single
|
||||
// user block, ordered by the tool call order of the preceding assistant message (matched
|
||||
@@ -75,13 +76,30 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ
|
||||
// non-thinking generation prompt ends with a bare </think> instead of an empty
|
||||
// <think></think> pair.
|
||||
const bool is_v4 = tmpl.source().find("function_calls") == std::string::npos;
|
||||
// V4.1: DSML tag names carry a leading space, e.g. <|DSML| calls>
|
||||
const bool is_v41 = is_v4 && tmpl.source().find("|DSML| calls>") != std::string::npos;
|
||||
|
||||
std::optional<json> adjusted_messages;
|
||||
if (is_v4) {
|
||||
adjusted_messages = deepseek_v4_sort_tool_results(inputs.messages);
|
||||
}
|
||||
|
||||
auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
|
||||
// V4.1 can declare tools on a system message instead of top-level "tools": parse calls to those too.
|
||||
json tools = inputs.tools;
|
||||
if (is_v41 && !(tools.is_array() && !tools.empty()) && inputs.messages.is_array()) {
|
||||
for (const auto & msg : inputs.messages) {
|
||||
if (msg.value("role", "") == "system" && msg.contains("tools") && msg.at("tools").is_array()) {
|
||||
if (!tools.is_array()) {
|
||||
tools = json::array();
|
||||
}
|
||||
for (const auto & tool : msg.at("tools")) {
|
||||
tools.push_back(tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto has_tools = tools.is_array() && !tools.empty();
|
||||
auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object();
|
||||
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
|
||||
auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
|
||||
@@ -90,17 +108,39 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ
|
||||
if (is_v4 && has_response_format) {
|
||||
additional_context = json{ { "response_format", inputs.json_schema } };
|
||||
}
|
||||
// V4.1 raises on anything but low/high/max or 1-100: map minimal -> low, xhigh -> max, medium -> default
|
||||
if (is_v41 && inputs.extra_context.is_object() && inputs.extra_context.contains("reasoning_effort") &&
|
||||
inputs.extra_context.at("reasoning_effort").is_string()) {
|
||||
const std::string effort = inputs.extra_context.at("reasoning_effort").get<std::string>();
|
||||
json mapped;
|
||||
if (effort == "minimal") {
|
||||
mapped = "low";
|
||||
} else if (effort == "xhigh") {
|
||||
mapped = "max";
|
||||
} else if (effort == "medium") {
|
||||
mapped = nullptr;
|
||||
}
|
||||
if (effort == "minimal" || effort == "xhigh" || effort == "medium") {
|
||||
LOG_DBG("%s: DeepSeek V4.1 reasoning_effort '%s' -> %s\n", __func__, effort.c_str(),
|
||||
mapped.is_null() ? "unset (template default)" : mapped.get<std::string>().c_str());
|
||||
if (!additional_context.has_value()) {
|
||||
additional_context = json::object();
|
||||
}
|
||||
(*additional_context)["reasoning_effort"] = mapped;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string DSML = "|DSML|";
|
||||
const std::string THINK_START = "<think>";
|
||||
const std::string THINK_END = "</think>";
|
||||
const std::string TC_BLOCK = is_v4 ? "tool_calls" : "function_calls";
|
||||
const std::string FC_START = "<" + DSML + TC_BLOCK + ">";
|
||||
const std::string FC_END = "</" + DSML + TC_BLOCK + ">";
|
||||
const std::string INVOKE_START = "<" + DSML + "invoke";
|
||||
const std::string INVOKE_END = "</" + DSML + "invoke>";
|
||||
const std::string PARAM_START = "<" + DSML + "parameter";
|
||||
const std::string PARAM_END = "</" + DSML + "parameter>";
|
||||
const std::string TAG_SP = is_v41 ? " " : "";
|
||||
const std::string TC_BLOCK = is_v41 ? "calls" : is_v4 ? "tool_calls" : "function_calls";
|
||||
const std::string FC_START = "<" + DSML + TAG_SP + TC_BLOCK + ">";
|
||||
const std::string FC_END = "</" + DSML + TAG_SP + TC_BLOCK + ">";
|
||||
const std::string INVOKE_START = "<" + DSML + TAG_SP + "invoke";
|
||||
const std::string INVOKE_END = "</" + DSML + TAG_SP + "invoke>";
|
||||
const std::string PARAM_START = "<" + DSML + TAG_SP + "parameter";
|
||||
const std::string PARAM_END = "</" + DSML + TAG_SP + "parameter>";
|
||||
const std::string GEN_PROMPT = "<|Assistant|>";
|
||||
const std::string TC_SEPARATOR = "\n\n";
|
||||
|
||||
@@ -152,7 +192,7 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ
|
||||
// build tool call section first since we might need it in reasoning
|
||||
auto tool_choice = p.choice();
|
||||
if (has_tool_calls) {
|
||||
foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) {
|
||||
foreach_function(tools, [&](size_t tool_index, const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
|
||||
@@ -223,7 +263,10 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ
|
||||
}
|
||||
|
||||
if (extract_reasoning && inputs.enable_thinking) {
|
||||
reasoning = p.optional(THINK_START + p.reasoning(p.until(THINK_END)) + THINK_END);
|
||||
// V4.1 renders a continuation without reasoning as "<|Assistant|></think>" even in thinking mode,
|
||||
// so consume the bare THINK_END too or it leaks into the content
|
||||
reasoning = is_v41 ? p.optional((THINK_START + p.reasoning(p.until(THINK_END)) + THINK_END) | p.literal(THINK_END))
|
||||
: p.optional(THINK_START + p.reasoning(p.until(THINK_END)) + THINK_END);
|
||||
reasoning_with_tc = THINK_START +
|
||||
p.reasoning(p.until_one_of({ TC_SEPARATOR + FC_START, FC_START, THINK_END })) +
|
||||
p.space() + obligatory_tool_calls;
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
#include "parsers.h"
|
||||
|
||||
// Harmony with XML tool calls: <|start|>assistant to=NAME<|channel|>analysis <|constrain|>xml<|message|><rfl:parameter name="KEY">\nVALUE\n</rfl:parameter>
|
||||
common_chat_params common_chat_params_init_harmony_xml(const common_chat_template & tmpl,
|
||||
const autoparser::generation_params & inputs) {
|
||||
common_chat_params data;
|
||||
|
||||
data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs);
|
||||
data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs);
|
||||
data.message_delimiters = {
|
||||
{ COMMON_CHAT_ROLE_ASSISTANT, "<|start|>assistant" },
|
||||
{ COMMON_CHAT_ROLE_USER, "<|start|>user" },
|
||||
{ COMMON_CHAT_ROLE_SYSTEM, "<|start|>developer" },
|
||||
{ COMMON_CHAT_ROLE_SYSTEM, "<|start|>system" },
|
||||
};
|
||||
|
||||
data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
|
||||
data.supports_thinking = true;
|
||||
|
||||
data.thinking_start_tag = "<|channel|>analysis<|message|>";
|
||||
data.thinking_end_tags = {"<|end|>"};
|
||||
|
||||
// EOG text must reach the parser: an XML call has no closing tag of its own
|
||||
static const std::vector<std::string> EOG = {
|
||||
"<|call|>", "<|return|>", "<|endoftext|>", "<|im_end|>", "<|fim_pad|>", "<|repo_name|>", "<|file_sep|>",
|
||||
};
|
||||
|
||||
data.preserved_tokens = {
|
||||
"<|channel|>", "<|constrain|>", "<|message|>", "<|start|>", "<|end|>",
|
||||
};
|
||||
data.preserved_tokens.insert(data.preserved_tokens.end(), EOG.begin(), EOG.end());
|
||||
|
||||
if (inputs.has_continuation()) {
|
||||
const auto & msg = inputs.continue_msg;
|
||||
|
||||
data.generation_prompt = "<|start|>assistant<|channel|>analysis<|message|>" + msg.reasoning_content;
|
||||
if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
|
||||
data.generation_prompt += "<|end|><|start|>assistant<|channel|>final<|message|>" + msg.render_content();
|
||||
}
|
||||
|
||||
data.prompt.append(data.generation_prompt);
|
||||
}
|
||||
|
||||
auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
|
||||
auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object();
|
||||
auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
|
||||
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
|
||||
|
||||
static const std::string PARAM_OPEN = "<rfl:parameter name=\"";
|
||||
static const std::string PARAM_CLOSE = "\n</rfl:parameter>";
|
||||
|
||||
data.parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
|
||||
auto start = p.rule("start", p.literal("<|start|>assistant"));
|
||||
|
||||
auto eog = p.choice();
|
||||
for (const auto & tok : EOG) {
|
||||
eog |= p.literal(tok);
|
||||
}
|
||||
auto with_eog = [&](std::vector<std::string> delims) {
|
||||
delims.insert(delims.end(), EOG.begin(), EOG.end());
|
||||
return delims;
|
||||
};
|
||||
|
||||
auto analysis_open = p.literal("<|channel|>analysis<|message|>");
|
||||
auto analysis_body = p.until_one_of(with_eog({ "<|end|>", "<|constrain|><|start|>assistant", "<|start|>assistant" }));
|
||||
auto analysis_close = p.optional(p.literal("<|end|>") | p.literal("<|constrain|>") | eog);
|
||||
if (extract_reasoning) {
|
||||
p.rule("analysis", analysis_open + p.reasoning(analysis_body) + analysis_close);
|
||||
} else {
|
||||
p.rule("analysis", p.atomic(p.content(analysis_open)) + p.content(analysis_body + analysis_close));
|
||||
}
|
||||
auto analysis = p.ref("analysis");
|
||||
|
||||
auto final_msg = p.rule("final",
|
||||
p.literal("<|channel|>final<|message|>") +
|
||||
p.content(p.until_one_of(with_eog({ "<|end|>" }))) +
|
||||
p.optional(p.literal("<|end|>") | eog));
|
||||
|
||||
if (has_response_format) {
|
||||
auto constrain_type = p.chars("[A-Za-z0-9_-]", 1, -1);
|
||||
auto constraint = p.optional(p.space() + p.optional(p.literal("<|constrain|>")) + constrain_type);
|
||||
auto response_format = p.rule("response-format",
|
||||
p.literal("<|channel|>final") + constraint + p.literal("<|message|>") +
|
||||
p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)) +
|
||||
p.optional(p.literal("<|end|>") | eog));
|
||||
|
||||
return start + p.zero_or_more(analysis + start) + response_format;
|
||||
}
|
||||
|
||||
if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE) {
|
||||
// tolerate a missing closing tag
|
||||
std::vector<std::string> value_ends = with_eog({ PARAM_OPEN, "<|end|>" });
|
||||
std::vector<std::string> string_ends = { PARAM_CLOSE };
|
||||
auto next_open = p.choice();
|
||||
for (const auto & e : value_ends) {
|
||||
string_ends.push_back("\n" + e);
|
||||
next_open |= p.literal(e);
|
||||
}
|
||||
auto arg_close = p.tool_arg_close(p.gbnf(
|
||||
p.literal(PARAM_CLOSE) | (p.literal("\n") + p.peek(p.optional(p.literal("\n")) + next_open)),
|
||||
gbnf_format_literal("\n") + " " + gbnf_format_literal(PARAM_CLOSE.substr(1)) + "?"));
|
||||
auto arg_string = p.rule("rfl-arg-string",
|
||||
p.ac(p.tool_arg_string_value(p.until_one_of(string_ends)) + arg_close, PARAM_CLOSE));
|
||||
auto arg_sep = p.optional(p.literal("\n"));
|
||||
|
||||
std::string call_close_gbnf = gbnf_format_literal("<|end|>");
|
||||
for (const auto & tok : EOG) {
|
||||
call_close_gbnf += " | " + gbnf_format_literal(tok);
|
||||
}
|
||||
auto call_close = p.gbnf(p.literal("<|end|>") | eog | p.peek(p.literal("<|start|>assistant")),
|
||||
"(" + call_close_gbnf + ")?");
|
||||
|
||||
auto tool_choice = p.choice();
|
||||
foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
|
||||
std::vector<common_peg_parser> arg_rules;
|
||||
foreach_parameter(function, [&](size_t param_index, const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) {
|
||||
auto rule_name = "tool-" + std::to_string(tool_index) + "-arg-" + std::to_string(param_index);
|
||||
auto arg_open = p.tool_arg_open(p.literal(PARAM_OPEN) + p.tool_arg_name(p.literal(param.name)) + p.literal("\">\n"));
|
||||
|
||||
auto types = param.schema->value_types();
|
||||
auto arg_value = p.eps();
|
||||
if (!types.has(common_chat_schema::TYPE_STRING) &&
|
||||
(types.has(common_chat_schema::TYPE_OBJECT) || types.has(common_chat_schema::TYPE_ARRAY))) {
|
||||
// the schema grammar fixes the key order, and the model writes keys in its own order
|
||||
arg_value = p.tool_arg_json_value(p.json()) + arg_close;
|
||||
} else if (!types.has(common_chat_schema::TYPE_STRING)) {
|
||||
arg_value = p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", doc, *param.schema)) + arg_close;
|
||||
} else if (types.is_only(common_chat_schema::TYPE_STRING)) {
|
||||
arg_value = arg_string;
|
||||
} else {
|
||||
// grammar keeps only the string rule; the parser tries JSON first to type the value
|
||||
auto json_value = p.choice();
|
||||
if (types.has(common_chat_schema::TYPE_OBJECT)) {
|
||||
json_value |= p.json_object();
|
||||
}
|
||||
if (types.has(common_chat_schema::TYPE_ARRAY)) {
|
||||
json_value |= p.json_array();
|
||||
}
|
||||
if (types.has(common_chat_schema::TYPE_NUMBER) || types.has(common_chat_schema::TYPE_INTEGER)) {
|
||||
json_value |= p.json_number();
|
||||
}
|
||||
if (types.has(common_chat_schema::TYPE_BOOLEAN)) {
|
||||
json_value |= p.json_bool();
|
||||
}
|
||||
if (types.has(common_chat_schema::TYPE_NULL)) {
|
||||
json_value |= p.json_null();
|
||||
}
|
||||
arg_value = p.gbnf(p.atomic(p.tool_arg_json_value(json_value) + arg_close) | arg_string, "rfl-arg-string");
|
||||
}
|
||||
|
||||
arg_rules.push_back(p.rule(rule_name, p.tool_arg(arg_open + arg_value)));
|
||||
});
|
||||
|
||||
if (arg_rules.empty()) {
|
||||
auto any_name = p.chars("[^\"\\n<]", 1, -1);
|
||||
arg_rules.push_back(p.rule("tool-" + std::to_string(tool_index) + "-arg-any",
|
||||
p.tool_arg(p.tool_arg_open(p.literal(PARAM_OPEN) + p.tool_arg_name(any_name) + p.literal("\">\n")) + arg_string)));
|
||||
}
|
||||
|
||||
auto args = p.zero_or_more(p.choice(arg_rules) + arg_sep);
|
||||
|
||||
auto call = p.tool(
|
||||
p.tool_open(p.literal(" to=") + p.tool_name(p.literal(name)) +
|
||||
p.literal("<|channel|>analysis <|constrain|>xml<|message|>")) +
|
||||
p.tool_args(args) +
|
||||
p.tool_close(call_close));
|
||||
|
||||
tool_choice |= p.rule("tool-" + std::to_string(tool_index), call);
|
||||
});
|
||||
|
||||
auto tool_calls = inputs.parallel_tool_calls
|
||||
? tool_choice + p.zero_or_more(start + tool_choice)
|
||||
: tool_choice;
|
||||
auto tool_call = p.trigger_rule("tool-call", tool_calls);
|
||||
|
||||
if (inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED) {
|
||||
return start + p.zero_or_more(analysis + start) + tool_call;
|
||||
}
|
||||
|
||||
return start + p.zero_or_more(analysis + start) + (tool_call | final_msg | p.content(p.rest()));
|
||||
}
|
||||
|
||||
auto unsolicited = p.rule("unsolicited", p.atomic(p.literal(" to=") + p.rest()));
|
||||
|
||||
// output without a channel header is returned as content instead of failing the request
|
||||
return start + p.zero_or_more(analysis + start) + (final_msg | unsolicited | p.content(p.rest()));
|
||||
});
|
||||
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
data.parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
if (data.grammar_lazy) {
|
||||
data.grammar_triggers = {
|
||||
{ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN, "^\\s+to$" },
|
||||
{ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN, "<\\|start\\|>assistant(\\s+to)" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "parsers.h"
|
||||
|
||||
// An assistant turn is rendered as one or more messages, each
|
||||
// "<|start|>assistant to=<recipient><|message|>{content}{END}" where END is
|
||||
// <|eom|> (more messages follow) or <|eot|> (end of turn):
|
||||
// - chain-of-thought: to=self, terminated by <|eom|>
|
||||
// - user facing aside: to=commentary, terminated by <|eom|>
|
||||
// - final answer: to=user, terminated by <|eot|>
|
||||
// A commentary message carries content the user sees while the assistant keeps
|
||||
// working, typically before or between tool calls.
|
||||
// The generation prompt is just "<|start|>assistant"; the model emits its own
|
||||
// " to=...<|message|>".
|
||||
common_chat_params common_chat_params_init_muse_spark(const common_chat_template & tmpl,
|
||||
const autoparser::generation_params & inputs) {
|
||||
common_chat_params data;
|
||||
|
||||
data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs);
|
||||
data.generation_prompt = "<|start|>assistant";
|
||||
data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
|
||||
data.supports_thinking = true;
|
||||
|
||||
data.preserved_tokens = {
|
||||
"<|start|>", "<|message|>", "<|eom|>", "<|eot|>",
|
||||
// ATEM tool-call markup emitted on " to=<tool>" turns.
|
||||
"<atem:function_calls>", "<atem:invoke", "<atem:parameter", "</atem:parameter>",
|
||||
"</atem:invoke>", "</atem:function_calls>",
|
||||
};
|
||||
|
||||
data.message_delimiters = {
|
||||
{ COMMON_CHAT_ROLE_ASSISTANT, "<|start|>assistant" },
|
||||
{ COMMON_CHAT_ROLE_USER, "<|start|>user" },
|
||||
{ COMMON_CHAT_ROLE_SYSTEM, "<|start|>system" },
|
||||
{ COMMON_CHAT_ROLE_TOOL, "<|start|>tool" },
|
||||
};
|
||||
|
||||
if (inputs.has_continuation()) {
|
||||
const auto & msg = inputs.continue_msg;
|
||||
|
||||
data.generation_prompt = "<|start|>assistant to=self<|message|>" + msg.reasoning_content;
|
||||
if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
|
||||
data.generation_prompt += "<|eom|><|start|>assistant to=user<|message|>" + msg.render_content();
|
||||
}
|
||||
|
||||
data.prompt.append(data.generation_prompt);
|
||||
}
|
||||
|
||||
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
|
||||
|
||||
auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
|
||||
// Constrained grammar whenever tools are offered.
|
||||
auto include_grammar = has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE;
|
||||
|
||||
data.parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
|
||||
auto start = p.rule("start", p.literal("<|start|>assistant"));
|
||||
|
||||
if (!extract_reasoning && !include_grammar) {
|
||||
return start + p.content(p.rest());
|
||||
}
|
||||
|
||||
if (extract_reasoning) {
|
||||
p.rule("analysis", p.literal(" to=self<|message|>") + p.reasoning(p.until("<|eom|>")) + p.literal("<|eom|>"));
|
||||
} else {
|
||||
p.rule("analysis", p.literal(" to=self<|message|>") + p.content(p.until("<|eom|>")) + p.literal("<|eom|>"));
|
||||
}
|
||||
auto analysis = p.ref("analysis");
|
||||
|
||||
// Commentary is user facing content emitted mid turn, so it reads as
|
||||
// content and always ends on <|eom|>.
|
||||
auto preamble = p.rule("preamble", p.literal(" to=commentary<|message|>") +
|
||||
p.content(p.until("<|eom|>")) + p.literal("<|eom|>"));
|
||||
|
||||
auto aside = p.rule("aside", analysis | preamble);
|
||||
|
||||
auto recipient = p.optional(p.literal(" to=user"));
|
||||
auto final_msg = p.rule("final", recipient + p.literal("<|message|>") +
|
||||
p.content(p.until_one_of({ "<|eot|>", "<|eom|>" })));
|
||||
|
||||
if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE) {
|
||||
auto string_value = p.ac(
|
||||
p.tool_arg_string_value(p.until("</atem:parameter>")) + p.tool_arg_close(p.literal("</atem:parameter>")),
|
||||
"</atem:parameter>");
|
||||
|
||||
auto tool_choice = p.choice();
|
||||
foreach_function(inputs.tools, [&](size_t tool_index, const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
const std::string name = function.at("name");
|
||||
|
||||
std::vector<common_peg_parser> arg_rules;
|
||||
foreach_parameter(function, [&](size_t param_index, const common_chat_schema_property & prop, const common_chat_schema_document_ptr & doc) {
|
||||
auto value_parser = p.eps();
|
||||
if (prop.schema->may_be_string()) {
|
||||
value_parser = string_value;
|
||||
} else {
|
||||
value_parser = p.tool_arg_json_value(
|
||||
p.schema(p.json(), "tool-" + std::to_string(tool_index) + "-arg-" + std::to_string(param_index) + "-schema", doc, *prop.schema))
|
||||
+ p.tool_arg_close(p.literal("</atem:parameter>"));
|
||||
}
|
||||
|
||||
arg_rules.push_back(p.tool_arg(
|
||||
p.tool_arg_open(p.literal("<atem:parameter name=\"") + p.tool_arg_name(p.literal(prop.name)) + p.literal("\">")) +
|
||||
value_parser));
|
||||
});
|
||||
|
||||
auto args = p.eps();
|
||||
if (!arg_rules.empty()) {
|
||||
args = p.zero_or_more(p.choice(arg_rules) + p.space());
|
||||
}
|
||||
|
||||
auto tool_parser = p.tool(
|
||||
p.tool_open(p.literal(" to=") + p.until("<|message|>") +
|
||||
p.literal("<|message|><atem:function_calls>") + p.space() +
|
||||
p.literal("<atem:invoke name=\"") + p.tool_name(p.literal(name)) + p.literal("\">") + p.space())
|
||||
<< p.tool_args(args)
|
||||
<< p.tool_close(p.literal("</atem:invoke>") + p.space() + p.literal("</atem:function_calls>")));
|
||||
|
||||
tool_choice |= p.rule("tool-" + std::to_string(tool_index), tool_parser);
|
||||
});
|
||||
|
||||
auto tool_calls = inputs.parallel_tool_calls
|
||||
? p.trigger_rule("tool-call", tool_choice + p.zero_or_more(p.literal("<|eom|>") + start + tool_choice))
|
||||
: p.trigger_rule("tool-call", tool_choice);
|
||||
|
||||
|
||||
if (inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED) {
|
||||
return p.zero_or_more(start + aside) + start + tool_calls;
|
||||
}
|
||||
auto trailing_calls = p.optional(p.literal("<|eom|>") + start + tool_calls);
|
||||
return p.zero_or_more(start + aside) + start + (tool_calls | (final_msg + trailing_calls));
|
||||
}
|
||||
|
||||
return p.zero_or_more(start + aside) + start + final_msg;
|
||||
});
|
||||
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
data.parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
data.grammar_triggers = {
|
||||
{ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN,
|
||||
"<\\|start\\|>assistant( to=(?!self<\\|message\\|>)(?!commentary<\\|message\\|>)(?!user<\\|message\\|>)[^<]*?<\\|message\\|>)" },
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -59,6 +59,8 @@ common_chat_params common_chat_params_init_gigachat_v3(const common_chat_templat
|
||||
|
||||
common_chat_params common_chat_params_init_gpt_oss(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
|
||||
|
||||
common_chat_params common_chat_params_init_harmony_xml(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
|
||||
|
||||
common_chat_params common_chat_params_init_k2_horizon(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
|
||||
|
||||
common_chat_params common_chat_params_init_kimi_k2(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
|
||||
@@ -79,6 +81,7 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template
|
||||
common_chat_params common_chat_params_init_ministral_3(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
|
||||
|
||||
common_chat_params common_chat_params_init_muse_glimmer(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
|
||||
common_chat_params common_chat_params_init_muse_spark(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
|
||||
|
||||
common_chat_params common_chat_params_init_qwen3_coder(const common_chat_template & tmpl, const autoparser::generation_params & inputs);
|
||||
|
||||
|
||||
@@ -76,9 +76,10 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat
|
||||
|
||||
auto reasoning = p.eps();
|
||||
if (supports_reasoning && extract_reasoning) {
|
||||
// The template strips the newline before </think>, so leave it out of the reasoning
|
||||
reasoning = p.optional("<think>" + p.space() +
|
||||
p.reasoning(p.until_one_of({ "</think>", "<tool_call>" })) +
|
||||
(p.literal("</think>") | p.peek(p.literal("<tool_call>"))));
|
||||
p.reasoning(p.until_one_of({ "\n</think>", "</think>", "<tool_call>" })) +
|
||||
(p.literal("\n</think>") | p.literal("</think>") | p.peek(p.literal("<tool_call>"))));
|
||||
}
|
||||
|
||||
// Response format parser
|
||||
@@ -166,8 +167,15 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat
|
||||
auto calls = inputs.parallel_tool_calls ? tool_call_first + p.zero_or_more(tool_call) : tool_call_first;
|
||||
auto tool_calls = p.trigger_rule("tool-call-root", p.repeat(calls, min_calls, 1));
|
||||
|
||||
// The templates separate content from the tool calls with one or two newlines,
|
||||
// stop the content before them
|
||||
std::vector<std::string> content_ends;
|
||||
for (const auto & start : tool_call_starts) {
|
||||
content_ends.insert(content_ends.end(), { "\n\n" + start, "\n" + start, start });
|
||||
}
|
||||
|
||||
return generation_prompt +
|
||||
(reasoning << p.content(p.until_one_of(tool_call_starts)) << tool_calls);
|
||||
(reasoning << p.content(p.until_one_of(content_ends)) << tool_calls);
|
||||
}
|
||||
|
||||
// Content only parser
|
||||
|
||||
@@ -9,6 +9,7 @@ set(LLAMA_CHAT_PARSERS_SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/gemma4.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/gigachat-v3.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/gpt-oss.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/harmony-xml.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/k2-horizon.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/kimi-k2.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/kimi-k3.cpp
|
||||
@@ -19,6 +20,7 @@ set(LLAMA_CHAT_PARSERS_SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/minimax-m3.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/ministral3.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/muse-glimmer.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/muse-spark.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/qwen3-coder.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/translate-gemma.cpp
|
||||
)
|
||||
|
||||
+30
-17
@@ -10,10 +10,12 @@
|
||||
#include <initializer_list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <regex>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
|
||||
// The parser can run with a lower limit than COMMON_RECURSIVE_LIMIT
|
||||
static constexpr int RULE_RECURSIVE_LIMIT = 64;
|
||||
|
||||
// Trick to catch missing branches
|
||||
template <typename T>
|
||||
inline constexpr bool is_always_false_v = false;
|
||||
@@ -186,13 +188,17 @@ std::string common_peg_ast_node::sanitized_text() const {
|
||||
}
|
||||
|
||||
void common_peg_ast_arena::visit(common_peg_ast_id id, const common_peg_ast_visitor & visitor) const {
|
||||
if (id == COMMON_PEG_INVALID_AST_ID) {
|
||||
return;
|
||||
}
|
||||
const auto & node = get(id);
|
||||
visitor(node);
|
||||
for (const auto & child : node.children) {
|
||||
visit(child, visitor);
|
||||
// no recursion: the AST is several nodes deep per level of the input
|
||||
std::vector<common_peg_ast_id> stack = { id };
|
||||
while (!stack.empty()) {
|
||||
const auto cur = stack.back();
|
||||
stack.pop_back();
|
||||
if (cur == COMMON_PEG_INVALID_AST_ID) {
|
||||
continue;
|
||||
}
|
||||
const auto & node = get(cur);
|
||||
visitor(node);
|
||||
stack.insert(stack.end(), node.children.rbegin(), node.children.rend());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,8 +721,9 @@ struct parser_executor {
|
||||
return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_SUCCESS, start_pos, pos, {}, std::move(invalid_utf8));
|
||||
}
|
||||
|
||||
if (match == common_trie::PARTIAL_MATCH) {
|
||||
// Found a partial match extending to end of input, return everything before it
|
||||
if (match == common_trie::PARTIAL_MATCH && !ctx.is_complete()) {
|
||||
// Found a partial match extending to end of input, return everything before it.
|
||||
// On a complete input the delimiter can no longer form, so the bytes belong to the text
|
||||
return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_SUCCESS, start_pos, pos, {}, std::move(invalid_utf8));
|
||||
}
|
||||
|
||||
@@ -736,8 +743,15 @@ struct parser_executor {
|
||||
}
|
||||
|
||||
common_peg_parse_result operator()(const common_peg_rule_parser & p) {
|
||||
// only rules recurse, a recursive rule is entered once per input level
|
||||
if (ctx.rule_depth[p.child] >= RULE_RECURSIVE_LIMIT) {
|
||||
throw common_peg_recursion_error("input is nested more than " + std::to_string(RULE_RECURSIVE_LIMIT) + " levels deep");
|
||||
}
|
||||
|
||||
// Parse the child
|
||||
ctx.rule_depth[p.child]++;
|
||||
auto result = arena.parse(p.child, ctx, start_pos);
|
||||
ctx.rule_depth[p.child]--;
|
||||
|
||||
if (!result.fail()) {
|
||||
std::string_view text;
|
||||
@@ -819,6 +833,10 @@ common_peg_parse_result common_peg_arena::parse(common_peg_parse_context & ctx,
|
||||
if (root_ == COMMON_PEG_INVALID_PARSER_ID) {
|
||||
throw std::runtime_error("No root parser set");
|
||||
}
|
||||
|
||||
// Initialize rule depth for all parsers
|
||||
ctx.rule_depth.assign(parsers_.size(), 0);
|
||||
|
||||
return parse(root_, ctx, start);
|
||||
}
|
||||
|
||||
@@ -1069,11 +1087,6 @@ common_peg_parser operator|(const std::string & str, const common_peg_parser & p
|
||||
return operator|(str.c_str(), p);
|
||||
}
|
||||
|
||||
static std::string rule_name(const std::string & name) {
|
||||
static const std::regex invalid_rule_chars_re("[^a-zA-Z0-9-]+");
|
||||
return std::regex_replace(name, invalid_rule_chars_re, "-");
|
||||
}
|
||||
|
||||
common_peg_parser_builder::common_peg_parser_builder() {}
|
||||
|
||||
common_peg_parser common_peg_parser_builder::sequence(const std::vector<common_peg_parser_id> & parsers) {
|
||||
@@ -1155,14 +1168,14 @@ common_peg_parser common_peg_parser_builder::schema(const common_peg_parser & p,
|
||||
}
|
||||
|
||||
common_peg_parser common_peg_parser_builder::rule(const std::string & name, const common_peg_parser & p, bool trigger) {
|
||||
auto clean_name = rule_name(name);
|
||||
auto clean_name = gbnf_format_rule_name(name);
|
||||
auto rule_id = arena_.add_parser(common_peg_rule_parser{clean_name, p.id(), trigger});
|
||||
arena_.add_rule(clean_name, rule_id);
|
||||
return ref(clean_name);
|
||||
}
|
||||
|
||||
common_peg_parser common_peg_parser_builder::rule(const std::string & name, const std::function<common_peg_parser()> & builder_fn, bool trigger) {
|
||||
auto clean_name = rule_name(name);
|
||||
auto clean_name = gbnf_format_rule_name(name);
|
||||
if (arena_.has_rule(clean_name)) {
|
||||
return ref(clean_name);
|
||||
}
|
||||
|
||||
+11
-3
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -161,9 +162,10 @@ struct common_peg_parse_result {
|
||||
};
|
||||
|
||||
enum common_peg_parse_flags {
|
||||
COMMON_PEG_PARSE_FLAG_NONE = 0,
|
||||
COMMON_PEG_PARSE_FLAG_LENIENT = 1 << 0,
|
||||
COMMON_PEG_PARSE_FLAG_DEBUG = 1 << 1,
|
||||
COMMON_PEG_PARSE_FLAG_NONE = 0,
|
||||
COMMON_PEG_PARSE_FLAG_LENIENT = 1 << 0,
|
||||
COMMON_PEG_PARSE_FLAG_DEBUG = 1 << 1,
|
||||
COMMON_PEG_PARSE_FLAG_COMPLETE = 1 << 2,
|
||||
};
|
||||
|
||||
inline common_peg_parse_flags operator|(common_peg_parse_flags a, common_peg_parse_flags b) {
|
||||
@@ -182,6 +184,10 @@ inline common_peg_parse_flags operator~(common_peg_parse_flags a) {
|
||||
return static_cast<common_peg_parse_flags>(~int(a));
|
||||
}
|
||||
|
||||
struct common_peg_recursion_error : std::invalid_argument {
|
||||
using std::invalid_argument::invalid_argument;
|
||||
};
|
||||
|
||||
struct common_peg_parse_context {
|
||||
std::string input; // [h, e, l, l, o, _, w, o, r, l, d]
|
||||
std::vector<llama_token> tokens; // [id, -1, -1, -1, -1, id, -1, -1, -1, -1, -1]
|
||||
@@ -189,6 +195,7 @@ struct common_peg_parse_context {
|
||||
common_peg_ast_arena ast;
|
||||
|
||||
int parse_depth;
|
||||
std::vector<int> rule_depth; // index: rule child id
|
||||
|
||||
common_peg_parse_context(common_peg_parse_flags flags = COMMON_PEG_PARSE_FLAG_NONE)
|
||||
: flags(flags), parse_depth(0) {}
|
||||
@@ -203,6 +210,7 @@ struct common_peg_parse_context {
|
||||
|
||||
bool is_lenient() const { return flags & COMMON_PEG_PARSE_FLAG_LENIENT; }
|
||||
bool is_debug() const { return flags & COMMON_PEG_PARSE_FLAG_DEBUG; }
|
||||
bool is_complete() const { return flags & COMMON_PEG_PARSE_FLAG_COMPLETE; }
|
||||
};
|
||||
|
||||
class common_peg_arena;
|
||||
|
||||
@@ -136,7 +136,8 @@ static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_to
|
||||
// track the end sequence within forced_tokens so it is also reported on DONE
|
||||
const int32_t match = ctx->end_matcher.advance(token);
|
||||
ctx->force_pos++;
|
||||
if (ctx->force_pos >= ctx->forced_tokens.size()) {
|
||||
// an end tag that is already in the prompt (e.g. "<think></think>") also ends forcing
|
||||
if (match >= 0 || ctx->force_pos >= ctx->forced_tokens.size()) {
|
||||
ctx->state = REASONING_BUDGET_DONE;
|
||||
ctx->end_match = match;
|
||||
COM_TRC("%s", "forced sequence complete, done\n");
|
||||
|
||||
+114
-59
@@ -16,8 +16,10 @@
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <cinttypes>
|
||||
|
||||
#define SPC_DBG(fmt, ...) LOG_DBG("spec %12.*s: " fmt, 12, __func__, __VA_ARGS__)
|
||||
@@ -1009,6 +1011,10 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
|
||||
bool is_mrope = false;
|
||||
int32_t selector_top_k = 0;
|
||||
|
||||
// DFlash2 probabilistic drafting: one generator per sequence and the seed it was last seeded with (-1: reseed)
|
||||
std::vector<std::mt19937> dflash2_rng;
|
||||
std::vector<int64_t> dflash2_seed;
|
||||
|
||||
// draft-dspark: the draft carries a Markov head and uses an anchor-first block layout
|
||||
const bool is_dspark;
|
||||
|
||||
@@ -1059,6 +1065,8 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
|
||||
|
||||
selector_top_k = llama_model_dflash_selector_top_k(model_dft);
|
||||
is_dflash2 = selector_top_k > 0;
|
||||
dflash2_rng.resize(n_seq);
|
||||
dflash2_seed.assign(n_seq, -1);
|
||||
char lilicorr_meta[32] = {};
|
||||
lilicorr_top_k = llama_model_meta_val_str(model_dft, "lilicorr.candidate_topk", lilicorr_meta, sizeof(lilicorr_meta)) >= 0
|
||||
? std::atoi(lilicorr_meta) : 0;
|
||||
@@ -1157,6 +1165,9 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
|
||||
return;
|
||||
}
|
||||
|
||||
// reseed per request rather than per round, or two identical requests differ
|
||||
dflash2_seed[seq_id] = -1;
|
||||
|
||||
const int32_t N = (int32_t) prompt.size();
|
||||
if (N <= 0) {
|
||||
return;
|
||||
@@ -1337,29 +1348,76 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
|
||||
const float * lattice = llama_get_embeddings_nextn(ctx_dft);
|
||||
GGML_ASSERT(lattice && "DFlash2 selector produced no lattice");
|
||||
|
||||
// DFlash2 can instead sample each step and hand q to the verifier for rejection sampling
|
||||
const bool sample = is_dflash2 && params.probabilistic && dp.result_q;
|
||||
|
||||
// greedy keeps temperature 1, sampling uses the target's
|
||||
const float temp = sample ? dp.temp : 1.0f;
|
||||
|
||||
if (sample && dflash2_seed[seq_id] != dp.seed) {
|
||||
dflash2_seed[seq_id] = dp.seed;
|
||||
// the default seed means random; mixed so it differs from the target's, as in spec_retune
|
||||
dflash2_rng[seq_id].seed(dp.seed == LLAMA_DEFAULT_SEED ? std::random_device{}() : dp.seed ^ 0x85ebca6bu);
|
||||
}
|
||||
|
||||
std::vector<float> probs;
|
||||
std::vector<int32_t> sorted_idx;
|
||||
|
||||
int32_t predecessor = 0;
|
||||
for (int32_t i = 1; i < n_block_tokens; ++i) {
|
||||
for (int32_t i = 1; i < (is_dflash2 ? n_block_tokens : params.n_max + 1); ++i) {
|
||||
const float * row = lattice + (size_t) (beg + i) * n_embd_dec;
|
||||
const int32_t top_k = is_dflash2 ? selector_top_k : lilicorr_top_k;
|
||||
const float * scores = row + top_k + (size_t) predecessor * top_k;
|
||||
|
||||
predecessor = (int32_t) std::distance(scores,
|
||||
std::max_element(scores, scores + top_k));
|
||||
if (params.p_min > 0.0f) {
|
||||
// softmax(scores) at the argmax, i.e. 1 / sum(exp(s_k - s_max))
|
||||
if (params.p_min > 0.0f || sample) {
|
||||
// softmax(scores / temp) at the argmax, i.e. 1 / sum(exp((s_k - s_max) / temp))
|
||||
probs.resize(top_k);
|
||||
float sum = 0.0f;
|
||||
for (int32_t k = 0; k < top_k; ++k) {
|
||||
sum += std::exp(scores[k] - scores[predecessor]);
|
||||
probs[k] = std::exp((scores[k] - scores[predecessor]) / temp);
|
||||
sum += probs[k];
|
||||
}
|
||||
if (1.0f / sum < params.p_min) {
|
||||
break;
|
||||
}
|
||||
for (int32_t k = 0; k < top_k; ++k) {
|
||||
probs[k] /= sum;
|
||||
}
|
||||
}
|
||||
if (sample) {
|
||||
// ggml_top_k leaves candidates unordered, so walk them by token id to keep a seed's draft stable
|
||||
sorted_idx.resize(top_k);
|
||||
for (int32_t k = 0; k < top_k; ++k) {
|
||||
sorted_idx[k] = k;
|
||||
}
|
||||
std::sort(sorted_idx.begin(), sorted_idx.end(), [row](int32_t a, int32_t b) { return row[a] < row[b]; });
|
||||
|
||||
// a round-off overshoot keeps the argmax, which has q > 0
|
||||
double u = std::uniform_real_distribution<double>(0.0, 1.0)(dflash2_rng[seq_id]);
|
||||
for (const int32_t k : sorted_idx) {
|
||||
u -= probs[k];
|
||||
if (u < 0.0) {
|
||||
predecessor = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto & q = dp.result_q->emplace_back();
|
||||
q.reserve(top_k);
|
||||
for (int32_t k = 0; k < top_k; ++k) {
|
||||
q.push_back({ (llama_token) row[k], scores[k], probs[k] });
|
||||
}
|
||||
}
|
||||
result.push_back((llama_token) row[predecessor]);
|
||||
}
|
||||
|
||||
if (result.size() < (size_t) params.n_min) {
|
||||
result.clear();
|
||||
if (sample) {
|
||||
dp.result_q->clear();
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -2152,9 +2210,6 @@ struct common_speculative_impl_ngram_cache : public common_speculative_impl {
|
||||
|
||||
uint16_t n_draft;
|
||||
|
||||
bool save_dynamic;
|
||||
bool save_static;
|
||||
|
||||
struct seq_info {
|
||||
size_t cache_size = 0; // number of tokens in n-gram cache
|
||||
|
||||
@@ -2168,54 +2223,72 @@ struct common_speculative_impl_ngram_cache : public common_speculative_impl {
|
||||
common_speculative_impl_ngram_cache(
|
||||
const common_params_speculative & params,
|
||||
uint32_t n_seq,
|
||||
uint16_t n_draft,
|
||||
const std::string & path_static,
|
||||
const std::string & path_dynamic,
|
||||
bool save_dynamic,
|
||||
bool save_static)
|
||||
uint16_t n_draft)
|
||||
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_CACHE, n_seq, n_draft)
|
||||
, params(params.ngram_cache)
|
||||
, n_draft(n_draft)
|
||||
, save_dynamic(save_dynamic)
|
||||
, save_static(save_static)
|
||||
{
|
||||
const auto & ngram = params.ngram_cache;
|
||||
|
||||
SPC_TRC("%s", "adding speculative implementation 'ngram-cache'\n");
|
||||
SPC_TRC("- n_draft=%d, cache_static=%s, cache_dynamic=%s\n",
|
||||
n_draft,
|
||||
path_static.empty() ? "none" : path_static.c_str(),
|
||||
path_dynamic.empty() ? "none" : path_dynamic.c_str());
|
||||
ngram.lookup_cache_static.empty() ? "none" : ngram.lookup_cache_static.c_str(),
|
||||
ngram.lookup_cache_dynamic.empty() ? "none" : ngram.lookup_cache_dynamic.c_str());
|
||||
|
||||
sinfos.resize(n_seq);
|
||||
|
||||
if (!path_static.empty()) {
|
||||
if (!ngram.lookup_cache_static.empty()) {
|
||||
try {
|
||||
auto ngram_cache_static = common_ngram_cache_load(path_static);
|
||||
auto ngram_cache_static = common_ngram_cache_load(ngram.lookup_cache_static);
|
||||
|
||||
for (auto & sinfo : sinfos) {
|
||||
sinfo.ngram_cache_static = ngram_cache_static;
|
||||
}
|
||||
} catch (...) {
|
||||
SPC_ERR("failed to open static lookup cache: %s", path_static.c_str());
|
||||
} catch (const std::ifstream::failure &) {
|
||||
SPC_ERR("failed to open static lookup cache: %s\n", ngram.lookup_cache_static.c_str());
|
||||
GGML_ABORT("Couldn't read static lookup cache");
|
||||
}
|
||||
}
|
||||
|
||||
if (!path_dynamic.empty()) {
|
||||
if (!ngram.lookup_cache_dynamic.empty()) {
|
||||
try {
|
||||
auto ngram_cache_dynamic = common_ngram_cache_load(path_dynamic);
|
||||
auto ngram_cache_dynamic = common_ngram_cache_load(ngram.lookup_cache_dynamic);
|
||||
|
||||
for (auto & sinfo : sinfos) {
|
||||
sinfo.ngram_cache_dynamic = ngram_cache_dynamic;
|
||||
}
|
||||
} catch (...) {
|
||||
SPC_ERR("failed to open dynamic lookup cache: %s", path_dynamic.c_str());
|
||||
GGML_ABORT("Couldn't read dynamic lookup cache");
|
||||
} catch (const std::ifstream::failure &) {
|
||||
std::error_code ec;
|
||||
const bool exists = std::filesystem::exists(ngram.lookup_cache_dynamic, ec);
|
||||
if (!exists && !ec) {
|
||||
// missing file is created on destruction
|
||||
SPC_WRN("dynamic lookup cache file does not exist: %s, starting with empty cache\n",
|
||||
ngram.lookup_cache_dynamic.c_str());
|
||||
} else {
|
||||
SPC_ERR("failed to open dynamic lookup cache: %s\n", ngram.lookup_cache_dynamic.c_str());
|
||||
GGML_ABORT("Couldn't read dynamic lookup cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void begin(llama_seq_id /*seq_id*/, const llama_tokens & /*prompt*/) override {
|
||||
// noop
|
||||
~common_speculative_impl_ngram_cache() override {
|
||||
if (params.lookup_cache_dynamic.empty() || sinfos.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto & dynamic_cache = sinfos.front().ngram_cache_dynamic;
|
||||
for (auto & sinfo : sinfos) {
|
||||
common_ngram_cache_merge(dynamic_cache, sinfo.ngram_cache_context);
|
||||
}
|
||||
|
||||
SPC_INF("saving dynamic lookup cache at %s\n", params.lookup_cache_dynamic.c_str());
|
||||
common_ngram_cache_save(dynamic_cache, params.lookup_cache_dynamic);
|
||||
}
|
||||
|
||||
void begin(llama_seq_id seq_id, const llama_tokens & /*prompt*/) override {
|
||||
sinfos[seq_id].cache_size = 0;
|
||||
}
|
||||
|
||||
void draft_one(
|
||||
@@ -2226,22 +2299,6 @@ struct common_speculative_impl_ngram_cache : public common_speculative_impl {
|
||||
|
||||
const auto & prompt = *dparams.prompt;
|
||||
|
||||
if (sinfo.cache_size < prompt.size() + 1) {
|
||||
llama_tokens tokens_new;
|
||||
tokens_new.reserve(prompt.size() + 1 - sinfo.cache_size);
|
||||
for (size_t j = sinfo.cache_size; j < prompt.size(); ++j) {
|
||||
tokens_new.push_back(prompt[j]);
|
||||
}
|
||||
tokens_new.push_back(dparams.id_last); // add the last token
|
||||
|
||||
// Update context ngram cache with new dparams.prompt:
|
||||
common_ngram_cache_update(
|
||||
sinfo.ngram_cache_context,
|
||||
LLAMA_NGRAM_MIN, LLAMA_NGRAM_MAX,
|
||||
tokens_new, tokens_new.size(), false);
|
||||
sinfo.cache_size = prompt.size() + 1;
|
||||
}
|
||||
|
||||
llama_tokens inp;
|
||||
inp.reserve(prompt.size() + 1);
|
||||
for (size_t j = 0; j < prompt.size(); ++j) {
|
||||
@@ -2249,6 +2306,16 @@ struct common_speculative_impl_ngram_cache : public common_speculative_impl {
|
||||
}
|
||||
inp.push_back(dparams.id_last);
|
||||
|
||||
if (sinfo.cache_size < inp.size()) {
|
||||
// Update context ngram cache with the full history so n-grams
|
||||
// ending at the new tokens have their preceding context
|
||||
common_ngram_cache_update(
|
||||
sinfo.ngram_cache_context,
|
||||
LLAMA_NGRAM_MIN, LLAMA_NGRAM_MAX,
|
||||
inp, inp.size() - sinfo.cache_size, false);
|
||||
sinfo.cache_size = inp.size();
|
||||
}
|
||||
|
||||
result.push_back(dparams.id_last);
|
||||
|
||||
common_ngram_cache_draft(
|
||||
@@ -2309,20 +2376,12 @@ static common_ngram_map get_common_ngram_map(
|
||||
return common_ngram_map(size_key, size_value, key_only, min_hits);
|
||||
}
|
||||
|
||||
static common_speculative_impl_ngram_cache create_state_ngram_cache(
|
||||
static std::unique_ptr<common_speculative_impl_ngram_cache> create_state_ngram_cache(
|
||||
const common_speculative_config & config,
|
||||
uint32_t n_seq,
|
||||
const std::string & path_static,
|
||||
const std::string & path_dynamic) {
|
||||
uint32_t n_seq) {
|
||||
uint16_t n_draft = 8; // TODO get from config?
|
||||
|
||||
// TODO bool param in common/common.h to set save_static/save_dynamic?
|
||||
bool save_static = false;
|
||||
bool save_dynamic = false;
|
||||
|
||||
common_speculative_impl_ngram_cache state(config.params, n_seq, n_draft, path_static, path_dynamic, save_static, save_dynamic);
|
||||
|
||||
return state;
|
||||
return std::make_unique<common_speculative_impl_ngram_cache>(config.params, n_seq, n_draft);
|
||||
}
|
||||
|
||||
std::string common_speculative_type_name_str(const std::vector<common_speculative_type> & types) {
|
||||
@@ -2822,11 +2881,7 @@ common_speculative * common_speculative_init(common_params_speculative & params,
|
||||
break;
|
||||
}
|
||||
case COMMON_SPECULATIVE_TYPE_NGRAM_CACHE: {
|
||||
auto state = create_state_ngram_cache(
|
||||
config, n_seq,
|
||||
params.ngram_cache.lookup_cache_static,
|
||||
params.ngram_cache.lookup_cache_dynamic);
|
||||
impls.push_back(std::make_unique<common_speculative_impl_ngram_cache>(state));
|
||||
impls.push_back(create_state_ngram_cache(config, n_seq));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -73,7 +73,7 @@ struct common_speculative_draft_params {
|
||||
// the generated draft from the last _draft() call
|
||||
llama_tokens * result;
|
||||
|
||||
// candidate distribution per drafted token; set it to make draft-simple and draft-mtp sample
|
||||
// candidate distribution per drafted token; set it to make draft-simple, draft-mtp and draft-dflash (DFlash2 only) sample
|
||||
std::vector<std::vector<llama_token_data>> * result_q = nullptr;
|
||||
|
||||
// the target's temp and seed, read only when the drafter samples probabilistically
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -17,13 +17,16 @@ __all__ = [
|
||||
|
||||
|
||||
TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"NEOChatModel": "sensenova_u1",
|
||||
"AfmoeForCausalLM": "afmoe",
|
||||
"LagunaForCausalLM": "laguna",
|
||||
"FlintForCausalLM": "flint",
|
||||
"ApertusForCausalLM": "llama",
|
||||
"ArceeForCausalLM": "llama",
|
||||
"ArcticForCausalLM": "arctic",
|
||||
"AudioFlamingo3ForConditionalGeneration": "qwen",
|
||||
"BaiChuanForCausalLM": "baichuan",
|
||||
"BeamForCausalLM": "beam",
|
||||
"BaichuanForCausalLM": "baichuan",
|
||||
"BailingMoeForCausalLM": "bailingmoe",
|
||||
"BailingMoeV2ForCausalLM": "bailingmoe",
|
||||
@@ -67,6 +70,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"LingDSparkModel": "qwen",
|
||||
"DeepseekV4ForCausalLM": "deepseek",
|
||||
"DeepseekV4DSparkModel": "deepseek",
|
||||
"DeepseekV41ForCausalLM": "deepseek41",
|
||||
"DistilBertForMaskedLM": "bert",
|
||||
"DistilBertForSequenceClassification": "bert",
|
||||
"DistilBertModel": "bert",
|
||||
@@ -145,6 +149,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"JinaBertModel": "bert",
|
||||
"JinaEmbeddingsV5Model": "bert",
|
||||
"K2HorizonForCausalLM": "k2_horizon",
|
||||
"Kolibri1ForCausalLM": "kolibri1",
|
||||
"KORMoForCausalLM": "qwen",
|
||||
"KimiK25ForConditionalGeneration": "deepseek",
|
||||
"KimiK3ForConditionalGeneration": "kimi_k3",
|
||||
@@ -167,6 +172,11 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"D1OmniModel": "lfm2",
|
||||
"Lfm2Model": "lfm2",
|
||||
"Lfm2MoeForCausalLM": "lfm2",
|
||||
"Lfm3D1Model": "lfm3",
|
||||
"Lfm3ForCausalLM": "lfm3",
|
||||
"Lfm3OmniForConditionalGeneration": "lfm3",
|
||||
"Lfm3VlForConditionalGeneration": "lfm3",
|
||||
"LimiteForCausalLM": "limite",
|
||||
"Llama4ForCausalLM": "llama",
|
||||
"Llama4ForConditionalGeneration": "llama",
|
||||
"LlamaBidirectionalModel": "llama",
|
||||
@@ -192,13 +202,19 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"MiniCPMForCausalLM": "minicpm",
|
||||
"MiniCPMV4_6ForConditionalGeneration": "minicpm",
|
||||
"MiniCPMV4_7ForConditionalGeneration": "minicpm",
|
||||
"MiniCPMOCRForSequenceClassification": "qwen",
|
||||
"MiniCPMOcrForConditionalGeneration": "minicpm",
|
||||
"MiniMaxText01ForCausalLM": "minimax",
|
||||
"MiniMaxM1ForCausalLM": "minimax",
|
||||
"MiniMaxM2ForCausalLM": "minimax",
|
||||
"MiniMaxM3SparseForCausalLM": "minimax",
|
||||
"MiniMaxM3SparseForConditionalGeneration": "minimax",
|
||||
"Ministral3ForCausalLM": "mistral3",
|
||||
"Ministral3Model": "mistral3",
|
||||
"Mistral3ForConditionalGeneration": "mistral3",
|
||||
"Mistral3Model": "mistral3",
|
||||
"MistralLarge4ForConditionalGeneration": "mistral4",
|
||||
"VoxtralRealtimeForConditionalGeneration": "mistral3",
|
||||
"MistralForCausalLM": "llama",
|
||||
"MixtralForCausalLM": "llama",
|
||||
"ModernBertDecisionModel": "bert",
|
||||
@@ -220,8 +236,10 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"OlmoeForCausalLM": "olmo",
|
||||
"MuseGlimmerAssistantModel": "muse_glimmer",
|
||||
"MuseGlimmerForConditionalGeneration": "muse_glimmer",
|
||||
"MuseSparkForConditionalGeneration": "muse_spark",
|
||||
"OpenELMForCausalLM": "openelm",
|
||||
"OpenJevModel": "qwen",
|
||||
"H2OLightningModel": "h2o_lightning",
|
||||
"OrionForCausalLM": "orion",
|
||||
"PLMForCausalLM": "plm",
|
||||
"PLaMo2ForCausalLM": "plamo",
|
||||
@@ -276,6 +294,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"SeedOssForCausalLM": "olmo",
|
||||
"SmallThinkerForCausalLM": "smallthinker",
|
||||
"SmolLM3ForCausalLM": "llama",
|
||||
"SopranoTextModel": "soprano",
|
||||
"Spark2_5ForCausalLM": "spark2_5",
|
||||
"SolarOpenForCausalLM": "glm",
|
||||
"StableLMEpochForCausalLM": "stablelm",
|
||||
@@ -315,6 +334,7 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
|
||||
"VllmD3Model": "vllm_d3",
|
||||
"DeepseekOCR2ForCausalLM": "deepseek",
|
||||
"DeepseekOCRForCausalLM": "deepseek",
|
||||
"DeepseekV41ForCausalLM": "deepseek41",
|
||||
"DeepseekV4ForCausalLM": "deepseek",
|
||||
"Dots3NoteForCausalLM": "dots3",
|
||||
"Dots3NoteForConditionalGeneration": "dots3",
|
||||
@@ -344,6 +364,9 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
|
||||
"Lfm2AudioForConditionalGeneration": "lfm2",
|
||||
"D1OmniModel": "lfm2",
|
||||
"Lfm2VlForConditionalGeneration": "lfm2",
|
||||
"Lfm3AudioForConditionalGeneration": "lfm3",
|
||||
"Lfm3OmniForConditionalGeneration": "lfm3",
|
||||
"Lfm3VlForConditionalGeneration": "lfm3",
|
||||
"LightOnOCRForConditionalGeneration": "lighton_ocr",
|
||||
"Llama4ForConditionalGeneration": "llama4",
|
||||
"LlavaForConditionalGeneration": "llava",
|
||||
@@ -352,11 +375,17 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
|
||||
"MiniMaxM3SparseForConditionalGeneration": "minimax",
|
||||
"MiniCPMV4_6ForConditionalGeneration": "minicpm",
|
||||
"MiniCPMV4_7ForConditionalGeneration": "minicpm",
|
||||
"MiniCPMOCRForSequenceClassification": "qwen3vl",
|
||||
"MiniCPMOcrForConditionalGeneration": "minicpm",
|
||||
"Mistral3ForConditionalGeneration": "llava",
|
||||
"Mistral3Model": "llava",
|
||||
"NemotronH_Omni_Reasoning_V3": "nemotron",
|
||||
"NemotronH_Nano_VL_V2": "nemotron",
|
||||
"MuseGlimmerForConditionalGeneration": "muse_glimmer",
|
||||
"MuseSparkForConditionalGeneration": "muse_spark",
|
||||
"PaddleOCRVisionModel": "ernie",
|
||||
"Phi4ForCausalLMV": "phi",
|
||||
"NEOVisionModel": "sensenova_u1",
|
||||
"Qwen2AudioForConditionalGeneration": "ultravox",
|
||||
"Qwen2VLForConditionalGeneration": "qwenvl",
|
||||
"Qwen2VLModel": "qwenvl",
|
||||
@@ -365,10 +394,12 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
|
||||
"Qwen3ASRForConditionalGeneration": "qwen3vl",
|
||||
"Qwen3OmniMoeForConditionalGeneration": "qwen3vl",
|
||||
"PocketTTSModel": "pockettts",
|
||||
"SopranoModel": "soprano",
|
||||
"Qwen3TTSForConditionalGeneration": "qwen3tts",
|
||||
"Qwen3VLForConditionalGeneration": "qwen3vl",
|
||||
"Qwen3VLMoeForConditionalGeneration": "qwen3vl",
|
||||
"OpenJevModel": "qwen3vl",
|
||||
"H2OLightningModel": "h2o_lightning",
|
||||
"Qwen3_5ForConditionalGeneration": "qwen3vl",
|
||||
"Qwen3_5MoeForConditionalGeneration": "qwen3vl",
|
||||
"Qwen4ExpForConditionalGeneration": "qwen4exp",
|
||||
@@ -380,6 +411,7 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
|
||||
"UltravoxModel": "ultravox",
|
||||
"UnlimitedOCRForCausalLM": "deepseek",
|
||||
"VoxtralForConditionalGeneration": "ultravox",
|
||||
"VoxtralRealtimeForConditionalGeneration": "ultravox",
|
||||
"YoutuVLForConditionalGeneration": "youtuvl",
|
||||
}
|
||||
|
||||
|
||||
+351
-80
@@ -10,11 +10,13 @@ import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
from hashlib import sha256
|
||||
from typing import TYPE_CHECKING, Any, Callable, ContextManager, Iterable, Iterator, Literal, Sequence, TypeVar, cast
|
||||
from itertools import chain
|
||||
from math import prod
|
||||
from transformers import AutoConfig
|
||||
|
||||
import numpy as np
|
||||
@@ -154,6 +156,7 @@ class ModelBase:
|
||||
self.fuse_gate_up_exps = fuse_gate_up_exps
|
||||
self._gate_exp_buffer: dict[int, Tensor] = {}
|
||||
self._up_exp_buffer: dict[int, Tensor] = {}
|
||||
self._unfusable_gate_up_layers: set[int] = set()
|
||||
self.fuse_qkv = fuse_qkv
|
||||
self._q_buffer: dict[int, Tensor] = {}
|
||||
self._k_buffer: dict[int, Tensor] = {}
|
||||
@@ -174,7 +177,9 @@ class ModelBase:
|
||||
self._nvfp4_layer_algo: dict[str, str | None] = {} # per-layer quant_algo, keyed by HF module path
|
||||
self._prec_a4: dict[str, bool] = {} # gguf tensor name -> can use 4-bit (A4) activations
|
||||
self._fp8_as_q8 = fp8_as_q8
|
||||
self._preserve_fp8 = ftype == gguf.LlamaFileType.GUESSED and not fp8_as_q8
|
||||
self._fp8_dequantized: set[str] = set()
|
||||
self._fp8_e4m3_preserved: set[str] = set()
|
||||
|
||||
# Apply heuristics to figure out typical tensor encoding based on first tensor's dtype
|
||||
# NOTE: can't use field "torch_dtype" in config.json, because some finetunes lie.
|
||||
@@ -325,7 +330,7 @@ class ModelBase:
|
||||
|
||||
def dequant_model(self):
|
||||
# If all quantized tensors were already handled (e.g. pure NVFP4), skip
|
||||
if self._is_nvfp4 and not any(k.endswith((".weight_scale", ".weight_scale_inv")) for k in self.model_tensors):
|
||||
if self._is_nvfp4 and not any(k.endswith((".weight_scale", ".weight_scale_inv", ".input_scale", ".activation_scale", "_activation_scale", ".k_scale", ".v_scale")) for k in self.model_tensors):
|
||||
return
|
||||
|
||||
tensors_to_remove: list[str] = []
|
||||
@@ -349,6 +354,12 @@ class ModelBase:
|
||||
def dequant_simple(weight: Tensor, scale: Tensor, block_size: Sequence[int] | None = None) -> Tensor:
|
||||
scale = scale.float()
|
||||
|
||||
# ModelOpt block scales are pre-shaped for broadcasting, e.g. [out/128, 1, in/128, 1]
|
||||
if block_size is None and scale.ndim == 2 * weight.ndim and all(n == 1 for n in scale.shape[1::2]):
|
||||
assert all(w % n == 0 for w, n in zip(weight.shape, scale.shape[0::2]))
|
||||
block_size = [w // n for w, n in zip(weight.shape, scale.shape[0::2])]
|
||||
scale = scale.reshape(tuple(scale.shape[0::2]))
|
||||
|
||||
if block_size is not None:
|
||||
dim_offset = scale.ndim - len(block_size)
|
||||
for i, size in enumerate(block_size):
|
||||
@@ -439,7 +450,7 @@ class ModelBase:
|
||||
|
||||
return (unpacked * scale.unsqueeze(-1).float()).reshape(shape)
|
||||
|
||||
def dequant_fp8() -> None:
|
||||
def dequant_fp8(fp8_configs: list[dict[str, Any]] | None = None) -> None:
|
||||
for name in self.model_tensors.keys():
|
||||
if name.endswith(".weight_scale"):
|
||||
weight_name = name.removesuffix("_scale")
|
||||
@@ -448,14 +459,29 @@ class ModelBase:
|
||||
continue
|
||||
w = self.model_tensors[weight_name]
|
||||
s = self.model_tensors[name]
|
||||
block_size = None
|
||||
is_fp8_weight = False
|
||||
if self._fp8_as_q8:
|
||||
is_fp8_weight = w().dtype in (torch.float8_e4m3fn, torch.float8_e5m2)
|
||||
self.model_tensors[weight_name] = lambda w=w, s=s: dequant_simple(w(), s(), None)
|
||||
if self._fp8_as_q8 or fp8_configs is not None:
|
||||
weight = w()
|
||||
is_fp8_weight = weight.dtype in (torch.float8_e4m3fn, torch.float8_e5m2)
|
||||
if fp8_configs is not None:
|
||||
if not is_fp8_weight:
|
||||
raise ValueError(f"Expected remaining weight {weight_name!r} to be FP8, got {weight.dtype}")
|
||||
scale = s()
|
||||
is_channel_scale = scale.ndim >= 1 and scale.ndim <= weight.ndim and scale.shape[0] == weight.shape[0] and all(size == 1 for size in scale.shape[1:])
|
||||
if prod(scale.shape) != 1 and not is_channel_scale:
|
||||
block_configs = [c for c in fp8_configs if c.get("strategy") == "block"]
|
||||
if not block_configs or any(not c.get("block_structure") or c.get("group_size") is not None for c in block_configs):
|
||||
raise ValueError(f"FP8 weight {weight_name!r} with scale shape {list(scale.shape)} requires a block layout")
|
||||
block_sizes = {tuple(c["block_structure"]) for c in block_configs}
|
||||
if len(block_sizes) != 1:
|
||||
raise ValueError(f"FP8 weight {weight_name!r} has ambiguous block layouts")
|
||||
block_size = next(iter(block_sizes))
|
||||
self.model_tensors[weight_name] = lambda w=w, s=s, bs=block_size: dequant_simple(w(), s(), bs)
|
||||
tensors_to_remove.append(name)
|
||||
if is_fp8_weight:
|
||||
if self._fp8_as_q8 and is_fp8_weight:
|
||||
self._fp8_dequantized.add(weight_name)
|
||||
if name.endswith((".input_scale", ".k_scale", ".v_scale")):
|
||||
if name.endswith((".input_scale", ".activation_scale", "_activation_scale", ".k_scale", ".v_scale")):
|
||||
tensors_to_remove.append(name)
|
||||
|
||||
if quant_method == "bitnet":
|
||||
@@ -520,29 +546,37 @@ class ModelBase:
|
||||
nvfp4_compressed_tensors = self._is_nvfp4_compressed_tensors(quant_method, quant_format, groups)
|
||||
|
||||
if nvfp4_compressed_tensors:
|
||||
dequant_fp8()
|
||||
fp8_configs = [
|
||||
g["weights"] for g in groups.values()
|
||||
if isinstance(g, dict) and g.get("format") == "float-quantized"
|
||||
and g.get("weights", {}).get("type") == "float"
|
||||
and g.get("weights", {}).get("num_bits") == 8
|
||||
]
|
||||
dequant_fp8(fp8_configs)
|
||||
elif len(groups) > 1:
|
||||
raise NotImplementedError("Can't handle multiple config groups for compressed-tensors yet")
|
||||
elif quant_format == "float-quantized" or quant_format == "int-quantized" or quant_format == "naive-quantized":
|
||||
weight_config = tuple(groups.values())[0]["weights"]
|
||||
block_size = weight_config.get("block_structure", None)
|
||||
strategy = weight_config.get("strategy")
|
||||
assert strategy == "channel" or strategy == "block"
|
||||
assert weight_config.get("group_size") is None # didn't find a model using this yet
|
||||
is_fp8 = (
|
||||
quant_format == "float-quantized"
|
||||
and weight_config.get("type") == "float"
|
||||
and weight_config.get("num_bits") == 8
|
||||
)
|
||||
assert strategy in ("channel", "block") or (is_fp8 and strategy == "tensor")
|
||||
assert weight_config.get("group_size") is None # didn't find a model using this yet
|
||||
for name in self.model_tensors.keys():
|
||||
if name.endswith(".weight_scale"):
|
||||
weight_name = name.removesuffix("_scale")
|
||||
w = self.model_tensors[weight_name]
|
||||
s = self.model_tensors[name]
|
||||
self.model_tensors[weight_name] = lambda w=w, s=s: dequant_simple(w(), s(), block_size)
|
||||
self.model_tensors[weight_name] = lambda w=w, s=s, bs=block_size: dequant_simple(w(), s(), bs)
|
||||
tensors_to_remove.append(name)
|
||||
if self._fp8_as_q8 and is_fp8:
|
||||
self._fp8_dequantized.add(weight_name)
|
||||
if is_fp8 and name.endswith((".input_scale", ".activation_scale", "_activation_scale", ".k_scale", ".v_scale")):
|
||||
tensors_to_remove.append(name)
|
||||
elif quant_format == "pack-quantized":
|
||||
weight_config = tuple(groups.values())[0]["weights"]
|
||||
assert weight_config.get("strategy") == "group"
|
||||
@@ -568,7 +602,7 @@ class ModelBase:
|
||||
tensors_to_remove.append(base_name + "_zero_point")
|
||||
else:
|
||||
raise NotImplementedError(f"Quant format {quant_format!r} for method {quant_method!r} is not yet supported")
|
||||
elif quant_method == "modelopt":
|
||||
elif quant_method in ("modelopt", "modelopt_mixed"):
|
||||
dequant_fp8()
|
||||
elif quant_method is not None:
|
||||
raise NotImplementedError(f"Quant method is not yet supported: {quant_method!r}")
|
||||
@@ -580,6 +614,148 @@ class ModelBase:
|
||||
for name, value in new_tensors.items():
|
||||
self.model_tensors[name] = value
|
||||
|
||||
def _map_fp8_weight_names(self, name: str) -> tuple[str, ...]:
|
||||
new_name = self.tensor_map.get_name(name, try_suffixes=(".weight",))
|
||||
return (new_name,) if new_name is not None else ()
|
||||
|
||||
def _prepare_fp8_e4m3_tensors(self):
|
||||
if not self._preserve_fp8:
|
||||
return
|
||||
|
||||
scale_tensors: dict[str, list[tuple[int, float]] | np.ndarray] = {}
|
||||
input_scale_tensors: dict[str, list[tuple[int, float]] | np.ndarray] = {}
|
||||
consumed: list[str] = []
|
||||
candidates: dict[str, tuple[str, Tensor, tuple[str, ...], torch.Size]] = {}
|
||||
weight_dtypes: dict[str, torch.dtype] = {}
|
||||
scaled_weights: set[str] = set()
|
||||
expert_pattern = re.compile(r"(\.[^.]*experts)\.(\d+)\.")
|
||||
scale_types = {
|
||||
gguf.MODEL_TENSOR.ATTN_Q, gguf.MODEL_TENSOR.ATTN_K, gguf.MODEL_TENSOR.ATTN_V,
|
||||
gguf.MODEL_TENSOR.ATTN_OUT, gguf.MODEL_TENSOR.ATTN_QKV, gguf.MODEL_TENSOR.ATTN_GATE,
|
||||
gguf.MODEL_TENSOR.FFN_GATE, gguf.MODEL_TENSOR.FFN_UP, gguf.MODEL_TENSOR.FFN_DOWN,
|
||||
gguf.MODEL_TENSOR.FFN_GATE_SHEXP, gguf.MODEL_TENSOR.FFN_UP_SHEXP, gguf.MODEL_TENSOR.FFN_DOWN_SHEXP,
|
||||
gguf.MODEL_TENSOR.FFN_GATE_EXP, gguf.MODEL_TENSOR.FFN_UP_EXP, gguf.MODEL_TENSOR.FFN_DOWN_EXP,
|
||||
gguf.MODEL_TENSOR.SSM_IN, gguf.MODEL_TENSOR.SSM_OUT, gguf.MODEL_TENSOR.SSM_ALPHA, gguf.MODEL_TENSOR.SSM_BETA,
|
||||
gguf.MODEL_TENSOR.NEXTN_EH_PROJ, gguf.MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD, gguf.MODEL_TENSOR.OUTPUT,
|
||||
}
|
||||
|
||||
for scale_name in list(self.model_tensors.keys()):
|
||||
weight_name = None
|
||||
if scale_name.endswith(".weight_scale"):
|
||||
weight_name = scale_name.removesuffix("_scale")
|
||||
elif scale_name.endswith("_scale_inv"):
|
||||
weight_name = scale_name.removesuffix("_scale_inv")
|
||||
elif scale_name.endswith(".qscale_weight"):
|
||||
weight_name = scale_name.removesuffix("qscale_weight") + "weight"
|
||||
|
||||
if weight_name is None or weight_name not in self.model_tensors:
|
||||
continue
|
||||
|
||||
scaled_weights.add(weight_name)
|
||||
weight = self.model_tensors[weight_name]()
|
||||
weight_dtypes[weight_name] = weight.dtype
|
||||
if weight.dtype != torch.float8_e4m3fn or weight.ndim < 2:
|
||||
continue
|
||||
|
||||
scale = self.model_tensors[scale_name]()
|
||||
if prod(scale.shape) != 1:
|
||||
continue
|
||||
|
||||
merged_name = expert_pattern.sub(r"\1.", weight_name)
|
||||
new_names = self._map_fp8_weight_names(merged_name)
|
||||
if not new_names:
|
||||
continue
|
||||
if any((mapped := self.tensor_map.get_type_and_name(new_name, try_suffixes=(".weight",))) is None or mapped[0] not in scale_types for new_name in new_names):
|
||||
continue
|
||||
bid = next((int(part) for part in new_names[0].split(".") if part.isdecimal()), None)
|
||||
self._fp8_e4m3_preserved.add(weight_name)
|
||||
if any(new_name.endswith("_norm.weight") or self.tensor_force_quant(weight_name, new_name, bid, weight.ndim) != gguf.GGMLQuantizationType.F8_E4M3 for new_name in new_names):
|
||||
self._fp8_e4m3_preserved.discard(weight_name)
|
||||
continue
|
||||
candidates[weight_name] = (scale_name, scale, new_names, weight.shape)
|
||||
|
||||
expert_groups: dict[str, list[str]] = {}
|
||||
for name in self.model_tensors:
|
||||
if (match := expert_pattern.search(name)) and name.endswith(".weight"):
|
||||
expert_groups.setdefault(name[:match.end(1)], []).append(name)
|
||||
for names in expert_groups.values():
|
||||
for name in names:
|
||||
if name not in weight_dtypes:
|
||||
weight_dtypes[name] = self.model_tensors[name]().dtype
|
||||
dtypes = {weight_dtypes[name] for name in names}
|
||||
if torch.float8_e4m3fn not in dtypes:
|
||||
continue
|
||||
if dtypes == {torch.float8_e4m3fn} and not any(name in scaled_weights for name in names):
|
||||
continue
|
||||
# The converter merges expert projections together.
|
||||
if not all(name in candidates for name in names):
|
||||
for name in names:
|
||||
candidates.pop(name, None)
|
||||
self._fp8_e4m3_preserved.discard(name)
|
||||
if name not in scaled_weights:
|
||||
gen = self.model_tensors[name]
|
||||
self.model_tensors[name] = lambda gen=gen: gen().float()
|
||||
|
||||
for weight_name, (scale_name, scale, new_names, shape) in candidates.items():
|
||||
scale = LazyTorchTensor.to_eager(scale).float().flatten()
|
||||
|
||||
weight_prefix = weight_name.removesuffix(".weight")
|
||||
# Transformers fine-grained FP8 uses activation_scale while ModelOpt uses input_scale.
|
||||
# Ref: https://github.com/vllm-project/vllm/blob/main/vllm/model_executor/models/mistral3.py#L357-L361
|
||||
input_scale_name = next((name for name in (
|
||||
weight_prefix + ".input_scale",
|
||||
weight_prefix + ".activation_scale",
|
||||
weight_prefix + "_activation_scale",
|
||||
weight_prefix + ".qscale_act",
|
||||
) if name in self.model_tensors), None)
|
||||
input_scale = None
|
||||
if input_scale_name is not None:
|
||||
input_scale = self.model_tensors[input_scale_name]()
|
||||
if prod(input_scale.shape) != 1:
|
||||
raise ValueError(f"FP8 input scale {input_scale_name!r} must be a scalar")
|
||||
input_scale = LazyTorchTensor.to_eager(input_scale).float().flatten()
|
||||
consumed.append(input_scale_name)
|
||||
|
||||
consumed.append(scale_name)
|
||||
|
||||
expert_match = expert_pattern.search(weight_name)
|
||||
if expert_match:
|
||||
expert_id = int(expert_match.group(2))
|
||||
for new_name in new_names:
|
||||
target_name = new_name.replace(".weight", ".scale")
|
||||
entries = scale_tensors.setdefault(target_name, [])
|
||||
assert isinstance(entries, list)
|
||||
cast(list[tuple[int, float]], entries).append((expert_id, float(scale[0])))
|
||||
if input_scale is not None:
|
||||
target_name = new_name.replace(".weight", ".input_scale")
|
||||
entries = input_scale_tensors.setdefault(target_name, [])
|
||||
assert isinstance(entries, list)
|
||||
cast(list[tuple[int, float]], entries).append((expert_id, float(input_scale[0])))
|
||||
else:
|
||||
for new_name in new_names:
|
||||
mapped = self.tensor_map.get_type_and_name(new_name, try_suffixes=(".weight",)) if len(shape) == 3 else None
|
||||
is_packed_expert = mapped is not None and mapped[0] in (
|
||||
gguf.MODEL_TENSOR.FFN_GATE_EXP, gguf.MODEL_TENSOR.FFN_UP_EXP, gguf.MODEL_TENSOR.FFN_DOWN_EXP,
|
||||
)
|
||||
# Packed expert weights need one scale value per expert.
|
||||
n_scales = shape[0] if is_packed_expert else 1
|
||||
scale_tensors[new_name.replace(".weight", ".scale")] = np.repeat(scale.numpy(), n_scales)
|
||||
if input_scale is not None:
|
||||
input_scale_tensors[new_name.replace(".weight", ".input_scale")] = np.repeat(input_scale.numpy(), n_scales)
|
||||
|
||||
for name in consumed:
|
||||
self.model_tensors.pop(name, None)
|
||||
|
||||
for name, values in chain(scale_tensors.items(), input_scale_tensors.items()):
|
||||
if isinstance(values, list):
|
||||
values.sort(key=lambda item: item[0])
|
||||
scale = np.array([item[1] for item in values], dtype=np.float32)
|
||||
else:
|
||||
scale = values.astype(np.float32)
|
||||
if not np.allclose(scale, 1.0, atol=1e-6):
|
||||
logger.info(f" + {name} (FP8 scale, shape [{scale.size}])")
|
||||
self.gguf_writer.add_tensor(name, scale)
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
name, gen = item
|
||||
@@ -624,6 +800,26 @@ class ModelBase:
|
||||
raise ValueError(f"Can not map tensor {name!r}")
|
||||
return new_name
|
||||
|
||||
def prepare_gate_up_fusion(self) -> None:
|
||||
self._unfusable_gate_up_layers.clear()
|
||||
if not self.fuse_gate_up_exps or self._fp8_as_q8:
|
||||
return
|
||||
|
||||
for name, gen in self.model_tensors.items():
|
||||
merged_name = re.sub(r"(\.experts)\.\d+\.", r"\1.", name)
|
||||
mapped = self.tensor_map.get_type_and_name(merged_name, try_suffixes=(".weight",))
|
||||
if mapped is None:
|
||||
continue
|
||||
tensor_type, new_name = mapped
|
||||
if tensor_type not in (gguf.MODEL_TENSOR.FFN_GATE_EXP, gguf.MODEL_TENSOR.FFN_UP_EXP, gguf.MODEL_TENSOR.FFN_GATE_UP_EXP) or not new_name.endswith(".weight"):
|
||||
continue
|
||||
if gen().dtype == torch.float8_e4m3fn:
|
||||
bid = next(int(part) for part in new_name.split(".") if part.isdecimal())
|
||||
self._unfusable_gate_up_layers.add(bid)
|
||||
|
||||
for bid in sorted(self._unfusable_gate_up_layers):
|
||||
logger.warning(f"Skipping --fuse-gate-up-exps for layer {bid}: fusion of preserved FP8 expert projections is not supported")
|
||||
|
||||
def prepare_qkv_fusion(self) -> None:
|
||||
self._fusable_qkv_weight_layers.clear()
|
||||
self._fusable_qkv_bias_layers.clear()
|
||||
@@ -637,6 +833,7 @@ class ModelBase:
|
||||
}
|
||||
weights: dict[int, set[gguf.MODEL_TENSOR]] = {}
|
||||
biases: dict[int, set[gguf.MODEL_TENSOR]] = {}
|
||||
fp8_layers: set[int] = set()
|
||||
|
||||
for name in self.model_tensors:
|
||||
mapped = self.tensor_map.get_type_and_name(name, try_suffixes=(".weight", ".bias"))
|
||||
@@ -651,12 +848,19 @@ class ModelBase:
|
||||
continue
|
||||
if new_name.endswith(".weight"):
|
||||
weights.setdefault(bid, set()).add(tensor_type)
|
||||
if not self._fp8_as_q8 and self.model_tensors[name]().dtype == torch.float8_e4m3fn:
|
||||
fp8_layers.add(bid)
|
||||
elif new_name.endswith(".bias"):
|
||||
biases.setdefault(bid, set()).add(tensor_type)
|
||||
|
||||
for bid, weight_types in weights.items():
|
||||
bias_types = biases.get(bid, set())
|
||||
if weight_types == qkv_types and (not bias_types or bias_types == qkv_types):
|
||||
# NVFP4 weights use separate reordering and repacking hooks and are exported before this fusion pass.
|
||||
# Separate FP8 scales cannot be represented by one fused QKV scale.
|
||||
if bid in fp8_layers:
|
||||
logger.warning(f"Skipping --fuse-qkv for layer {bid}: fusion of preserved FP8 projections is not supported")
|
||||
continue
|
||||
self._fusable_qkv_weight_layers.add(bid)
|
||||
if bias_types:
|
||||
self._fusable_qkv_bias_layers.add(bid)
|
||||
@@ -845,7 +1049,8 @@ class ModelBase:
|
||||
new_name = self.map_tensor_name(name)
|
||||
|
||||
# Handle gate/up expert tensor fusion if enabled
|
||||
if self.fuse_gate_up_exps and bid is not None:
|
||||
# Preserved FP8 gate/up scales must stay with their separate projections.
|
||||
if self.fuse_gate_up_exps and bid is not None and bid not in self._unfusable_gate_up_layers:
|
||||
if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.FFN_GATE_EXP, bid):
|
||||
self._gate_exp_buffer[bid] = data_torch
|
||||
elif self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.FFN_UP_EXP, bid):
|
||||
@@ -902,8 +1107,46 @@ class ModelBase:
|
||||
|
||||
return [(new_name, data_torch)]
|
||||
|
||||
def _tensor_requires_f32(self, new_name: str, bid: int | None, n_dims: int) -> bool:
|
||||
# Conditions should closely match those in llama_model_quantize_internal in llama.cpp.
|
||||
return n_dims <= 1 or new_name.endswith("_norm.weight") or (
|
||||
any(
|
||||
self.match_model_tensor_name(new_name, key, bid)
|
||||
for key in (
|
||||
gguf.MODEL_TENSOR.FFN_GATE_INP,
|
||||
gguf.MODEL_TENSOR.FFN_GATE_INP_SHEXP,
|
||||
gguf.MODEL_TENSOR.POS_EMBD,
|
||||
gguf.MODEL_TENSOR.TOKEN_TYPES,
|
||||
gguf.MODEL_TENSOR.SSM_CONV1D,
|
||||
gguf.MODEL_TENSOR.SHORTCONV_CONV,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_FIRST,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_W1,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_W2,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_DECAY_W1,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_DECAY_W2,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_LERP_FUSED,
|
||||
gguf.MODEL_TENSOR.POSNET_NORM1,
|
||||
gguf.MODEL_TENSOR.POSNET_NORM2,
|
||||
gguf.MODEL_TENSOR.V_ENC_EMBD_POS,
|
||||
gguf.MODEL_TENSOR.A_ENC_EMBD_POS,
|
||||
gguf.MODEL_TENSOR.ALTUP_CORRECT_COEF,
|
||||
gguf.MODEL_TENSOR.ALTUP_PREDICT_COEF,
|
||||
# Kimi KDA conv weights should be F32
|
||||
gguf.MODEL_TENSOR.SSM_CONV1D_Q,
|
||||
gguf.MODEL_TENSOR.SSM_CONV1D_K,
|
||||
gguf.MODEL_TENSOR.SSM_CONV1D_V,
|
||||
# DSA indexer weights should be F32
|
||||
gguf.MODEL_TENSOR.INDEXER_PROJ,
|
||||
)
|
||||
)
|
||||
or new_name[-7:] not in (".weight", ".lora_a", ".lora_b")
|
||||
)
|
||||
|
||||
def tensor_force_quant(self, name: str, new_name: str, bid: int | None, n_dims: int) -> gguf.GGMLQuantizationType | bool:
|
||||
del new_name, bid # unused
|
||||
if name in self._fp8_e4m3_preserved and n_dims >= 2:
|
||||
if self._tensor_requires_f32(new_name, bid, n_dims):
|
||||
return gguf.GGMLQuantizationType.F32
|
||||
return gguf.GGMLQuantizationType.F8_E4M3
|
||||
# Force FP8-original tensors to Q8_0 when requested; Q8_0 is faster than F16/BF16.
|
||||
if self._fp8_as_q8 and name in self._fp8_dequantized and n_dims >= 2:
|
||||
return gguf.GGMLQuantizationType.Q8_0
|
||||
@@ -1034,7 +1277,7 @@ class ModelBase:
|
||||
expert_scales: dict[tuple[int, str], list[tuple[int, float]]] = {}
|
||||
expert_input_scales: dict[tuple[int, str], list[tuple[int, float]]] = {}
|
||||
expert_shapes: dict[tuple[int, str], list[int]] = {}
|
||||
n_experts = self.find_hparam(["num_local_experts", "num_experts"], optional=True) or 0
|
||||
n_experts = self.find_hparam(["num_local_experts", "num_experts", "n_routed_experts"], optional=True) or 0
|
||||
consumed: list[str] = []
|
||||
|
||||
for name in self.model_tensors.keys():
|
||||
@@ -1045,14 +1288,17 @@ class ModelBase:
|
||||
input_scale_name = name.replace(".weight", ".input_scale")
|
||||
if scale_name not in self.model_tensors:
|
||||
continue
|
||||
# Force eager materialization of lazy tensors
|
||||
weight = LazyTorchTensor.to_eager(self.model_tensors[name]())
|
||||
scale = LazyTorchTensor.to_eager(self.model_tensors[scale_name]())
|
||||
weight = self.model_tensors[name]()
|
||||
scale = self.model_tensors[scale_name]()
|
||||
|
||||
# Skip non-NVFP4 tensors(e.g. 1D scale, or float8 weight)
|
||||
if scale.ndim < 2 or weight.dtype in (torch.float8_e4m3fn, torch.float8_e5m2):
|
||||
# Leave FP8 weights and their scales for _prepare_fp8_e4m3_tensors.
|
||||
if weight.dtype in (torch.float8_e4m3fn, torch.float8_e5m2) or scale.ndim < 2:
|
||||
continue
|
||||
if weight.ndim != 2 or scale.shape[0] != weight.shape[0] or scale.shape[1] * 8 != weight.shape[1]:
|
||||
raise ValueError(f"NVFP4 weight {name!r} has incompatible shapes: weight {list(weight.shape)}, scale {list(scale.shape)}")
|
||||
|
||||
weight = LazyTorchTensor.to_eager(weight)
|
||||
scale = LazyTorchTensor.to_eager(scale)
|
||||
scale2 = LazyTorchTensor.to_eager(self.model_tensors.get(scale2_name, lambda: torch.tensor(1.0))())
|
||||
input_scale = LazyTorchTensor.to_eager(self.model_tensors.get(input_scale_name, lambda: torch.tensor(1.0))())
|
||||
|
||||
@@ -1208,8 +1454,10 @@ class ModelBase:
|
||||
self.model_tensors[input_scale_name] = inverse_scale(self.model_tensors.pop(name))
|
||||
self._generate_nvfp4_tensors()
|
||||
|
||||
self._prepare_fp8_e4m3_tensors()
|
||||
self.dequant_model()
|
||||
|
||||
self.prepare_gate_up_fusion()
|
||||
self.prepare_qkv_fusion()
|
||||
|
||||
# Handle empty tensor_map for models with block_count=0 (like MobileNetV5)
|
||||
@@ -1223,10 +1471,21 @@ class ModelBase:
|
||||
if name.endswith((".attention.masked_bias", ".attention.bias", ".rotary_emb.inv_freq")):
|
||||
continue
|
||||
|
||||
# FP8 transforms must only reorder values; norms and vectors use numeric F32 transforms.
|
||||
preserve_fp8_e4m3 = self._preserve_fp8 and data_torch.dtype == torch.float8_e4m3fn and data_torch.ndim >= 2 and not name.endswith(("norm.weight", "norm_f.weight"))
|
||||
if preserve_fp8_e4m3:
|
||||
for new_name in self._map_fp8_weight_names(name):
|
||||
mapped_bid = next((int(part) for part in new_name.split(".") if part.isdecimal()), None)
|
||||
if self._tensor_requires_f32(new_name, mapped_bid, data_torch.ndim):
|
||||
preserve_fp8_e4m3 = False
|
||||
break
|
||||
if preserve_fp8_e4m3:
|
||||
self._fp8_e4m3_preserved.add(name)
|
||||
|
||||
old_dtype = data_torch.dtype
|
||||
|
||||
# convert any unsupported data types to float32
|
||||
if data_torch.dtype not in (torch.float16, torch.float32):
|
||||
if data_torch.dtype not in (torch.float16, torch.float32) and name not in self._fp8_e4m3_preserved:
|
||||
data_torch = data_torch.to(torch.float32)
|
||||
|
||||
# use the first number-like part of the tensor name as the block id
|
||||
@@ -1237,51 +1496,14 @@ class ModelBase:
|
||||
break
|
||||
|
||||
for new_name, data_torch in (self.modify_tensors(data_torch, name, bid)):
|
||||
# TODO: why do we squeeze here?
|
||||
# data = data_torch.squeeze().numpy()
|
||||
data = data_torch.numpy()
|
||||
|
||||
n_dims = len(data.shape)
|
||||
n_dims = len(data_torch.shape)
|
||||
data_qtype: gguf.GGMLQuantizationType | bool = self.tensor_force_quant(name, new_name, bid, n_dims)
|
||||
|
||||
# Most of the codebase that takes in 1D tensors or norms only handles F32 tensors
|
||||
if n_dims <= 1 or new_name.endswith("_norm.weight"):
|
||||
data_qtype = gguf.GGMLQuantizationType.F32
|
||||
|
||||
# Conditions should closely match those in llama_model_quantize_internal in llama.cpp
|
||||
# Some tensor types are always in float32
|
||||
if data_qtype is False and (
|
||||
any(
|
||||
self.match_model_tensor_name(new_name, key, bid)
|
||||
for key in (
|
||||
gguf.MODEL_TENSOR.FFN_GATE_INP,
|
||||
gguf.MODEL_TENSOR.FFN_GATE_INP_SHEXP,
|
||||
gguf.MODEL_TENSOR.POS_EMBD,
|
||||
gguf.MODEL_TENSOR.TOKEN_TYPES,
|
||||
gguf.MODEL_TENSOR.SSM_CONV1D,
|
||||
gguf.MODEL_TENSOR.SHORTCONV_CONV,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_FIRST,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_W1,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_W2,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_DECAY_W1,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_DECAY_W2,
|
||||
gguf.MODEL_TENSOR.TIME_MIX_LERP_FUSED,
|
||||
gguf.MODEL_TENSOR.POSNET_NORM1,
|
||||
gguf.MODEL_TENSOR.POSNET_NORM2,
|
||||
gguf.MODEL_TENSOR.V_ENC_EMBD_POS,
|
||||
gguf.MODEL_TENSOR.A_ENC_EMBD_POS,
|
||||
gguf.MODEL_TENSOR.ALTUP_CORRECT_COEF,
|
||||
gguf.MODEL_TENSOR.ALTUP_PREDICT_COEF,
|
||||
# Kimi KDA conv weights should be F32
|
||||
gguf.MODEL_TENSOR.SSM_CONV1D_Q,
|
||||
gguf.MODEL_TENSOR.SSM_CONV1D_K,
|
||||
gguf.MODEL_TENSOR.SSM_CONV1D_V,
|
||||
# DSA indexer weights should be F32
|
||||
gguf.MODEL_TENSOR.INDEXER_PROJ,
|
||||
)
|
||||
)
|
||||
or new_name[-7:] not in (".weight", ".lora_a", ".lora_b")
|
||||
):
|
||||
if data_qtype is False and self._tensor_requires_f32(new_name, bid, n_dims):
|
||||
data_qtype = gguf.GGMLQuantizationType.F32
|
||||
|
||||
if data_qtype is False and any(
|
||||
@@ -1319,16 +1541,22 @@ class ModelBase:
|
||||
else:
|
||||
raise ValueError(f"Unknown file type: {self.ftype.name}")
|
||||
|
||||
if data_torch.dtype == torch.float8_e4m3fn:
|
||||
data = data_torch.view(torch.uint8).numpy() if data_qtype == gguf.GGMLQuantizationType.F8_E4M3 else data_torch.float().numpy()
|
||||
else:
|
||||
data = data_torch.numpy()
|
||||
|
||||
# a chunked tensor quantizes as one chunk at a time, while it is written
|
||||
quantize = data.quantize if isinstance(data, gguf.LazyChunkedTensor) else (
|
||||
lambda qtype, d=data: gguf.quants.quantize(d, qtype))
|
||||
|
||||
try:
|
||||
data = quantize(data_qtype)
|
||||
except gguf.QuantError as e:
|
||||
logger.warning("%s, %s", e, "falling back to F16")
|
||||
data_qtype = gguf.GGMLQuantizationType.F16
|
||||
data = quantize(data_qtype)
|
||||
if not (data_qtype == gguf.GGMLQuantizationType.F8_E4M3 and data.dtype == np.uint8):
|
||||
try:
|
||||
data = quantize(data_qtype)
|
||||
except gguf.QuantError as e:
|
||||
logger.warning("%s, %s", e, "falling back to F16")
|
||||
data_qtype = gguf.GGMLQuantizationType.F16
|
||||
data = quantize(data_qtype)
|
||||
|
||||
shape = gguf.quant_shape_from_byte_shape(data.shape, data_qtype) if data.dtype == np.uint8 else data.shape
|
||||
|
||||
@@ -1369,6 +1597,8 @@ class ModelBase:
|
||||
self.ftype = gguf.LlamaFileType.MOSTLY_NVFP4
|
||||
elif self._is_mxfp4:
|
||||
self.ftype = gguf.LlamaFileType.MOSTLY_MXFP4_MOE
|
||||
elif self._fp8_e4m3_preserved:
|
||||
self.ftype = gguf.LlamaFileType.MOSTLY_F8_E4M3
|
||||
|
||||
# Generate parameter weight class (useful for leader boards) if not yet determined
|
||||
if self.metadata.size_label is None and total_params > 0:
|
||||
@@ -1625,7 +1855,7 @@ class TextModel(ModelBase):
|
||||
self.gguf_writer.add_head_count_kv(n_head_kv)
|
||||
logger.info(f"gguf: key-value head count = {n_head_kv}")
|
||||
|
||||
if self.hparams.get("is_causal") is False:
|
||||
if self.hparams.get("is_causal") is False or self.hparams.get("use_bidirectional_attention") is True:
|
||||
self.gguf_writer.add_causal_attention(False)
|
||||
logger.info("gguf: causal attention = False")
|
||||
|
||||
@@ -1700,7 +1930,7 @@ class TextModel(ModelBase):
|
||||
self.gguf_writer.add_expert_group_used_count(n_group_used)
|
||||
logger.info(f"gguf: expert groups used count = {n_group_used}")
|
||||
|
||||
if (score_func := self.find_hparam(["score_function", "scoring_func", "score_func", "moe_router_activation", "moe_router_activation_func", "expert_selection_fn", "router_score_func"], optional=True)) is not None:
|
||||
if (score_func := self.find_hparam(["score_function", "scoring_func", "score_func", "moe_router_activation", "moe_router_activation_func", "expert_selection_fn", "router_score_func", "router_score_function"], optional=True)) is not None:
|
||||
if score_func == "sigmoid":
|
||||
self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SIGMOID)
|
||||
elif score_func == "softmax":
|
||||
@@ -1836,6 +2066,9 @@ class TextModel(ModelBase):
|
||||
if chkhsh == "1431a23e583c97432bc230bff598d103ddb5a1f89960c8f1d1051aaa944d0b35":
|
||||
# ref: https://huggingface.co/sapienzanlp/Minerva-7B-base-v1.0
|
||||
res = "minerva-7b"
|
||||
if chkhsh == "6e4d72c6e5260b90606e26a0931fa15e4b42ec6e97719d12919f2b1f93478262":
|
||||
# ref: https://huggingface.co/domyn/Domyn-Small-v1.0
|
||||
res = "domyn-small"
|
||||
if chkhsh == "7e57df22b1fe23a7b1e1c7f3dc4e3f96d43a4eb0836d0c6bdc3436d7b2f1c664":
|
||||
# ref: https://huggingface.co/tencent/Hunyuan-A13B-Instruct
|
||||
res = "hunyuan"
|
||||
@@ -1869,6 +2102,12 @@ class TextModel(ModelBase):
|
||||
if chkhsh == "66b8d4e19ab16c3bfd89bce5d785fb7e0155e8648708a1f42077cb9fe002c273":
|
||||
# ref: https://huggingface.co/alvarobartt/grok-2-tokenizer
|
||||
res = "grok-2"
|
||||
if chkhsh == "f5c965e4198da6e7525072d87d1ecf4c51d981fd486c7c4ec728ef9cec7e03fc":
|
||||
# ref: https://huggingface.co/meta-models/Muse-Spark-Open-816B-A42B
|
||||
res = "gpt-4o"
|
||||
if chkhsh == "6e040dfe72e4b85855588c53acf4909ac4e98e55bc3a33cd7db499180dc42a78":
|
||||
# ref: https://huggingface.co/Aleph-Alpha/Kolibri-1
|
||||
res = "qwen2"
|
||||
if chkhsh == "b3d1dd861f1d4c5c0d2569ce36baf3f90fe8a102db3de50dd71ff860d91be3df":
|
||||
# ref: https://huggingface.co/aari1995/German_Semantic_V3
|
||||
res = "jina-v2-de"
|
||||
@@ -1932,6 +2171,9 @@ class TextModel(ModelBase):
|
||||
if chkhsh == "52df12b4c8d4176e7481aab4b6e8454d1fd0a210a04a574f6d4e067d10e23c3e":
|
||||
# ref: https://huggingface.co/CohereLabs/North-Mini-Code-1.0
|
||||
res = "cohere2moe"
|
||||
if chkhsh == "d5420e601e979078c670bc503d9ac411c311fa76b76a76267c31e0d6ba816637":
|
||||
# ref: https://huggingface.co/RecursiveSI/flint
|
||||
res = "flint"
|
||||
if chkhsh == "e636dc30a262dcc0d8c323492e32ae2b70728f4df7dfe9737d9f920a282b8aea":
|
||||
# ref: https://huggingface.co/Qwen/Qwen1.5-7B
|
||||
res = "qwen2"
|
||||
@@ -2163,20 +2405,34 @@ class TextModel(ModelBase):
|
||||
special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
|
||||
special_vocab.add_to_gguf(self.gguf_writer)
|
||||
|
||||
def _is_hybriddna(self) -> bool:
|
||||
tokenizer_config_file = self.dir_model / "tokenizer_config.json"
|
||||
if not tokenizer_config_file.is_file():
|
||||
return False
|
||||
with open(tokenizer_config_file, "r", encoding="utf-8") as f:
|
||||
return json.load(f).get("tokenizer_class") == "HybridDNATokenizer"
|
||||
|
||||
def _set_vocab_hybriddna(self):
|
||||
from transformers import AutoTokenizer
|
||||
tokenizer = AutoTokenizer.from_pretrained(self.dir_model, trust_remote_code=True)
|
||||
vocab_size = self.hparams.get("vocab_size", len(tokenizer.vocab)) # ty: ignore[unresolved-attribute]
|
||||
assert max(tokenizer.vocab.values()) < vocab_size # ty: ignore[unresolved-attribute]
|
||||
vocab = tokenizer.get_vocab()
|
||||
vocab_size = self.hparams.get("vocab_size", len(vocab))
|
||||
assert max(vocab.values()) < vocab_size
|
||||
|
||||
reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()} # ty: ignore[unresolved-attribute]
|
||||
# k-mers can share text with a base-vocab BPE token (e.g. CCCCCC) and get
|
||||
# dropped by get_vocab(); a reserved marker suffix (U+E000) keeps each
|
||||
# k-mer's own id (llama.cpp strips it on detokenization)
|
||||
for kmer in tokenizer.kmers: # ty: ignore[unresolved-attribute]
|
||||
reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in vocab.items()}
|
||||
# k-mers can share text with a base-vocab BPE token (e.g. CCCCCC) and get dropped by get_vocab()
|
||||
# a reserved marker suffix (U+E000) keeps each k-mer's own id (llama.cpp strips it on detokenization)
|
||||
# tail="pad_n" (Carbon-14B) adds N-padded tail tokens ("CT" -> "CTNNNN") that get the same marker
|
||||
# llama.cpp detects them to pad with N and strips the padding on detokenization, tail="char" is unsupported
|
||||
tail = getattr(tokenizer, "tail", "pad_a")
|
||||
if tokenizer.k != 6 or tail not in ("pad_a", "pad_n"):
|
||||
raise NotImplementedError(f"HybridDNATokenizer with k={tokenizer.k}, tail={tail!r}")
|
||||
for kmer in [*tokenizer.kmers, *getattr(tokenizer, "npad_tokens", [])]:
|
||||
reverse_vocab[tokenizer.dna_token_to_id[kmer]] = kmer + "\ue000" # ty: ignore[unresolved-attribute]
|
||||
added_vocab = tokenizer.get_added_vocab() # ty: ignore[unresolved-attribute]
|
||||
added_tokens_decoder = tokenizer.added_tokens_decoder # ty: ignore[unresolved-attribute]
|
||||
# older HybridDNATokenizer versions (Carbon-500M/3B/8B) do not register the base tokenizer's added tokens (<|im_start|>, <think>, ...) as their own
|
||||
# but they are still encoded as special tokens: take their types from the base tokenizer
|
||||
base_tokenizer = getattr(tokenizer, "_base_tokenizer", tokenizer)
|
||||
added_tokens_decoder = {**base_tokenizer.added_tokens_decoder, **tokenizer.added_tokens_decoder} # ty: ignore[unresolved-attribute]
|
||||
|
||||
tokens: list[str] = []
|
||||
toktypes: list[int] = []
|
||||
@@ -2186,7 +2442,7 @@ class TextModel(ModelBase):
|
||||
toktypes.append(gguf.TokenType.UNUSED)
|
||||
else:
|
||||
token: str = reverse_vocab[i]
|
||||
if token in added_vocab:
|
||||
if i in added_tokens_decoder:
|
||||
if added_tokens_decoder[i].special or self.does_token_look_special(token):
|
||||
toktypes.append(gguf.TokenType.CONTROL)
|
||||
else:
|
||||
@@ -2198,10 +2454,19 @@ class TextModel(ModelBase):
|
||||
tokpre = self.get_vocab_base_pre(tokenizer)
|
||||
self.gguf_writer.add_tokenizer_model("hybriddna")
|
||||
self.gguf_writer.add_tokenizer_pre(tokpre)
|
||||
self.gguf_writer.add_dna_auto_tags(getattr(tokenizer, "auto_dna_tags", False))
|
||||
self.gguf_writer.add_token_list(tokens)
|
||||
self.gguf_writer.add_token_types(toktypes)
|
||||
|
||||
special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
|
||||
if not special_vocab.merges:
|
||||
# Carbon-500M/3B/8B ship only tokenizer.py, no tokenizer.json
|
||||
# take the merges and special token ids from the base BPE tokenizer, keeping the model's own chat template (if any)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base_tokenizer.save_pretrained(tmp)
|
||||
base_special_vocab = gguf.SpecialVocab(Path(tmp), load_merges=True)
|
||||
base_special_vocab.chat_template = special_vocab.chat_template
|
||||
special_vocab = base_special_vocab
|
||||
special_vocab.add_to_gguf(self.gguf_writer)
|
||||
|
||||
def _set_vocab_qwen(self):
|
||||
@@ -2636,7 +2901,13 @@ class TextModel(ModelBase):
|
||||
)
|
||||
|
||||
if vocab.tokenizer_type == MistralTokenizerType.tekken:
|
||||
self.gguf_writer.add_tokenizer_pre("tekken")
|
||||
with open(vocab.fname_tokenizer, encoding="utf-8") as f:
|
||||
pattern = json.load(f)["config"]["pattern"]
|
||||
# newer tekken.json split numbers into right-aligned groups of 3 digits instead of single digits
|
||||
if "\\p{N}{1,3}(?=(?:\\p{N}{3})*(?!\\p{N}))" in pattern:
|
||||
self.gguf_writer.add_tokenizer_pre("tekken-num3")
|
||||
else:
|
||||
self.gguf_writer.add_tokenizer_pre("tekken")
|
||||
self.gguf_writer.add_token_merges(
|
||||
vocab.extract_vocab_merges_from_model()
|
||||
)
|
||||
@@ -3113,7 +3384,7 @@ def get_model_architecture(hparams: dict[str, Any], model_type: ModelType) -> st
|
||||
# Kimi-K3's text_config reports "KimiLinearForCausalLM", which is the older
|
||||
# Kimi-Linear-48B architecture and cannot load K3 (no attention residuals,
|
||||
# latent MoE, situ, ...). Route on the top-level architecture instead.
|
||||
if model_type == ModelType.TEXT and arch in ("StepVLForConditionalGeneration", "Sarashina2VisionForCausalLM", "Exaone4_5_ForConditionalGeneration", "Step3p7ForConditionalGeneration", "KimiK3ForConditionalGeneration"):
|
||||
if model_type == ModelType.TEXT and arch in ("NEOChatModel", "StepVLForConditionalGeneration", "Sarashina2VisionForCausalLM", "Exaone4_5_ForConditionalGeneration", "Step3p7ForConditionalGeneration", "KimiK3ForConditionalGeneration"):
|
||||
return arch
|
||||
|
||||
# if "architectures" is found in the sub-config, use that instead
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Iterable, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import ModelBase, TextModel, gguf, logger
|
||||
|
||||
|
||||
@ModelBase.register("BeamForCausalLM")
|
||||
class BeamModel(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.BEAM
|
||||
|
||||
# reference NoPE query-scale constants; not in config.json
|
||||
NOPE_SCALE_POWER = 0.1
|
||||
NOPE_SCALE_LENGTH = 2048
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
hp = self.hparams
|
||||
rope = hp.get("rope_parameters") or {}
|
||||
if rope.get("full_attention") is not None:
|
||||
raise ValueError("Beam: expected NoPE full-attention layers (rope_parameters.full_attention = null)")
|
||||
if "sliding_attention" in rope:
|
||||
swa_rope = rope["sliding_attention"] or {}
|
||||
else:
|
||||
# flat rope_theta / rope_scaling, already normalized by TextModel
|
||||
swa_rope = {k: v for k, v in self.rope_parameters.items() if k not in ("sliding_attention", "full_attention", "type")}
|
||||
swa_rope.update(self.rope_parameters.get("sliding_attention") or {})
|
||||
if swa_rope.get("rope_type", "default") != "default":
|
||||
raise ValueError(f"Beam: unsupported sliding-attention rope type {swa_rope.get('rope_type')!r}")
|
||||
# full layers are NoPE, so mirror the sliding rope params for the base class
|
||||
swa_rope = {k: v for k, v in swa_rope.items() if not (k == "rope_type" and v == "default")}
|
||||
self.rope_parameters = {"sliding_attention": dict(swa_rope), "full_attention": dict(swa_rope)}
|
||||
|
||||
layer_types = hp["layer_types"]
|
||||
mlp_types = hp["mlp_layer_types"]
|
||||
n_layer = hp["num_hidden_layers"]
|
||||
if len(layer_types) != n_layer or len(mlp_types) != n_layer:
|
||||
raise ValueError("Beam: layer_types / mlp_layer_types must have num_hidden_layers entries")
|
||||
if any(t not in ("sliding_attention", "full_attention") for t in layer_types):
|
||||
raise ValueError(f"Beam: unknown layer type in {layer_types}")
|
||||
if "sliding_attention" not in layer_types:
|
||||
raise ValueError("Beam: expected at least one sliding_attention layer")
|
||||
n_dense = 0
|
||||
while n_dense < n_layer and mlp_types[n_dense] == "dense":
|
||||
n_dense += 1
|
||||
if any(t != "sparse" for t in mlp_types[n_dense:]):
|
||||
raise ValueError("Beam: llama.cpp supports dense MLP layers only as a leading block")
|
||||
self._n_dense_lead = n_dense
|
||||
if hp.get("attention_bias", False):
|
||||
raise ValueError("Beam: attention_bias=True is not supported")
|
||||
self._experts: dict[tuple[int, str], dict[int, Tensor]] = {}
|
||||
|
||||
def set_vocab(self):
|
||||
self._set_vocab_gpt2()
|
||||
# otherwise EOT is guessed from a hash-ordered scan
|
||||
from transformers import AutoTokenizer
|
||||
tok = AutoTokenizer.from_pretrained(self.dir_model)
|
||||
ret_id = tok.convert_tokens_to_ids("<|return|>") # ty: ignore[unresolved-attribute]
|
||||
if isinstance(ret_id, int) and ret_id >= 0:
|
||||
self.gguf_writer.add_eot_token_id(ret_id)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
hp = self.hparams
|
||||
|
||||
head_dim = hp["head_dim"]
|
||||
self.gguf_writer.add_rope_dimension_count(head_dim)
|
||||
|
||||
self.gguf_writer.add_sliding_window(hp["sliding_window"])
|
||||
self.gguf_writer.add_sliding_window_pattern([t == "sliding_attention" for t in hp["layer_types"]])
|
||||
|
||||
self.gguf_writer.add_attn_temperature_scale(self.NOPE_SCALE_POWER)
|
||||
self.gguf_writer.add_attn_temperature_length(self.NOPE_SCALE_LENGTH)
|
||||
|
||||
self.gguf_writer.add_leading_dense_block_count(self._n_dense_lead)
|
||||
self.gguf_writer.add_expert_feed_forward_length(hp["moe_intermediate_size"])
|
||||
self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SIGMOID)
|
||||
self.gguf_writer.add_expert_weights_norm(bool(hp.get("norm_topk_prob", True)))
|
||||
# equivalent to the reference's post-aggregation scaling
|
||||
self.gguf_writer.add_expert_weights_scale(float(hp["routed_scaling_factor"]))
|
||||
|
||||
if (softcap := hp.get("final_logit_softcapping")) is not None:
|
||||
self.gguf_writer.add_final_logit_softcapping(float(softcap))
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
name, gen = item
|
||||
if name.endswith(".self_attn.gate_bias"):
|
||||
name = name.replace(".self_attn.gate_bias", ".self_attn.gate_proj.bias")
|
||||
return super().filter_tensors((name, gen))
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
hp = self.hparams
|
||||
if name.endswith(".self_attn.gate_proj.bias"):
|
||||
assert tuple(data_torch.shape) == (hp["num_attention_heads"],), f"{name}: one gate bias per head expected, got {tuple(data_torch.shape)}"
|
||||
elif name.endswith(".self_attn.gate_proj.weight"):
|
||||
assert tuple(data_torch.shape) == (hp["num_attention_heads"] * hp["head_dim"], hp["hidden_size"]), f"{name}: {tuple(data_torch.shape)}"
|
||||
elif name.endswith(".mlp.gate.e_score_correction.bias"):
|
||||
assert tuple(data_torch.shape) == (hp["num_experts"],), f"{name}: {tuple(data_torch.shape)}"
|
||||
elif name.endswith((".self_attn.q_norm.weight", ".self_attn.k_norm.weight")):
|
||||
assert tuple(data_torch.shape) == (hp["head_dim"],), f"{name}: per-head q/k norm expected, got {tuple(data_torch.shape)}"
|
||||
elif bid is not None and bid < self._n_dense_lead and ".mlp.experts." in name:
|
||||
raise ValueError(f"Beam: expert tensor {name} in a dense layer")
|
||||
if ".mlp.experts." in name:
|
||||
assert bid is not None
|
||||
n_experts = self.hparams["num_experts"]
|
||||
parts = name.split(".")
|
||||
xid = int(parts[parts.index("experts") + 1])
|
||||
proj = parts[parts.index("experts") + 2]
|
||||
if proj not in ("gate_proj", "up_proj", "down_proj"):
|
||||
raise ValueError(f"Beam: unexpected expert tensor {name}")
|
||||
bucket = self._experts.setdefault((bid, proj), {})
|
||||
if xid in bucket:
|
||||
raise ValueError(f"Beam: duplicate expert tensor {name}")
|
||||
bucket[xid] = data_torch
|
||||
if len(bucket) < n_experts:
|
||||
return
|
||||
del self._experts[(bid, proj)]
|
||||
data = torch.stack([bucket[e] for e in range(n_experts)], dim=0)
|
||||
yield from super().modify_tensors(data, f"model.layers.{bid}.mlp.experts.{proj}.weight", bid)
|
||||
return
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
def prepare_tensors(self):
|
||||
super().prepare_tensors()
|
||||
if self._experts:
|
||||
missing = sorted(f"layer {b} {p} ({len(v)} experts)" for (b, p), v in self._experts.items())
|
||||
raise ValueError(f"Beam: unprocessed experts: {missing}")
|
||||
logger.info("Beam: all routed experts stacked")
|
||||
+11
-8
@@ -236,6 +236,12 @@ class DeepseekModel(TextModel):
|
||||
class DeepseekV2Model(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.DEEPSEEK2
|
||||
|
||||
def _map_fp8_weight_names(self, name: str) -> tuple[str, ...]:
|
||||
# MLA projections do not have runtime weight-scale support.
|
||||
if name.endswith((".q_a_proj.weight", ".q_b_proj.weight", ".kv_a_proj_with_mqa.weight", ".kv_b_proj.weight")):
|
||||
return ()
|
||||
return super()._map_fp8_weight_names(name)
|
||||
|
||||
# TODO @ngxson : remove this when we support MTP for deepseek models
|
||||
skip_mtp = True
|
||||
|
||||
@@ -243,7 +249,7 @@ class DeepseekV2Model(TextModel):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
hparams: dict = ModelBase.load_hparams(self.dir_model, is_mistral_format=False)
|
||||
hparams: dict = ModelBase.load_hparams(self.dir_model, is_mistral_format=self.is_mistral_format)
|
||||
self.origin_hf_arch = hparams.get('architectures', [None])[0]
|
||||
|
||||
# special handling for Deepseek OCR
|
||||
@@ -803,9 +809,6 @@ class DeepseekV4Model(TextModel):
|
||||
self._dsv4_mxfp4_generated = True
|
||||
return ()
|
||||
|
||||
def _format_dsv4_tensor_name(self, key: gguf.MODEL_TENSOR, bid: int | None, suffix: str = ".weight") -> str:
|
||||
return self.format_tensor_name(key, bid, suffix)
|
||||
|
||||
def _map_dsv4_tensor_name(self, name: str, bid: int | None) -> tuple[gguf.MODEL_TENSOR, str]:
|
||||
root_map: dict[str, tuple[gguf.MODEL_TENSOR, str]] = {
|
||||
"embed.weight": (gguf.MODEL_TENSOR.TOKEN_EMBD, ".weight"),
|
||||
@@ -879,17 +882,17 @@ class DeepseekV4Model(TextModel):
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if re.match(r"layers\.\d+\.ffn\.experts\.\d+\.w[123]\.(weight|scale)$", name):
|
||||
return []
|
||||
return
|
||||
|
||||
# hash layers route text tokens via tid2eid and image tokens via bias_vl; gate.bias is unused
|
||||
if name.endswith(".ffn.gate.bias") and bid is not None and bid < self.hparams["num_hash_layers"]:
|
||||
return []
|
||||
return
|
||||
|
||||
tensor_key, suffix = self._map_dsv4_tensor_name(name, bid)
|
||||
if tensor_key == gguf.MODEL_TENSOR.FFN_GATE_TID2EID:
|
||||
return []
|
||||
return
|
||||
|
||||
return [(self._format_dsv4_tensor_name(tensor_key, bid, suffix), data_torch)]
|
||||
yield self.format_tensor_name(tensor_key, bid, suffix), data_torch
|
||||
|
||||
def tensor_force_quant(self, name: str, new_name: str, bid: int | None, n_dims: int) -> gguf.GGMLQuantizationType | bool:
|
||||
del bid # unused
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Iterable, TYPE_CHECKING, cast
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import LazyTorchTensor, ModelBase, TextModel, gguf
|
||||
from .deepseek import DeepseekV4FlashVisionModel, DeepseekV4Model
|
||||
|
||||
|
||||
# Engram hash constants from inference/engram.py, stored in the GGUF: the token map needs the HF tokenizers
|
||||
# normalizers and the multipliers numpy's PCG64 stream, neither of which the runtime has.
|
||||
|
||||
def _is_prime(n: int) -> bool:
|
||||
if n < 2 or n % 2 == 0:
|
||||
return n == 2
|
||||
return all(n % d for d in range(3, math.isqrt(n) + 1, 2))
|
||||
|
||||
|
||||
def engram_primes(n_layers: int, max_ngram_size: int, n_heads: int, vocab_size: int) -> list[list[list[int]]]:
|
||||
"""EngramLayout.from_args: [layer][ngram order 2..max][head] bucket moduli, never reused across layers."""
|
||||
seen: set[int] = set()
|
||||
out: list[list[list[int]]] = []
|
||||
for _ in range(n_layers):
|
||||
per_ngram = []
|
||||
for _ in range(max_ngram_size - 1):
|
||||
sizes, cur = [], vocab_size - 1
|
||||
for _ in range(n_heads):
|
||||
cand = cur + 1
|
||||
while not _is_prime(cand) or cand in seen:
|
||||
cand += 1
|
||||
cur = cand
|
||||
seen.add(cur)
|
||||
sizes.append(cur)
|
||||
per_ngram.append(sizes)
|
||||
out.append(per_ngram)
|
||||
return out
|
||||
|
||||
|
||||
def engram_multipliers(layer_ids: list[int], max_ngram_size: int, compressed_vocab_size: int) -> list[list[int]]:
|
||||
"""compute_hash_multipliers: odd, bounded so that token_id * multiplier cannot overflow int64."""
|
||||
bound = max(1, (np.iinfo(np.int64).max // compressed_vocab_size) // 2)
|
||||
rows = []
|
||||
for layer_id in layer_ids:
|
||||
rng = np.random.default_rng(10007 * layer_id)
|
||||
values = rng.integers(low=0, high=bound, size=(max_ngram_size,), dtype=np.int64)
|
||||
rows.append([int(v) * 2 + 1 for v in values])
|
||||
return rows
|
||||
|
||||
|
||||
def engram_compressed_token_map(tokenizer_dir: Path) -> tuple[list[int], int]:
|
||||
"""build_compressed_token_map: token id -> compressed id (tokens that normalize alike collapse)."""
|
||||
from tokenizers import Regex, normalizers
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(tokenizer_dir)
|
||||
sentinel = "\ue000"
|
||||
normalizer = normalizers.Sequence([
|
||||
normalizers.NFKC(),
|
||||
normalizers.NFD(),
|
||||
normalizers.StripAccents(),
|
||||
normalizers.Lowercase(),
|
||||
normalizers.Replace(Regex(r"[ \t\r\n]+"), " "),
|
||||
normalizers.Replace(Regex(r"^ $"), sentinel),
|
||||
normalizers.Strip(),
|
||||
normalizers.Replace(sentinel, " "),
|
||||
])
|
||||
backend = tokenizer.backend_tokenizer
|
||||
key_to_new: dict[str, int] = {}
|
||||
lookup = [0] * len(tokenizer)
|
||||
for token_id in range(len(tokenizer)):
|
||||
text = backend.decode([token_id], skip_special_tokens=False)
|
||||
if "\ufffd" in text:
|
||||
key = backend.id_to_token(token_id)
|
||||
else:
|
||||
normalized = normalizer.normalize_str(text)
|
||||
key = normalized if normalized else text
|
||||
lookup[token_id] = key_to_new.setdefault(key, len(key_to_new))
|
||||
return lookup, len(key_to_new)
|
||||
|
||||
|
||||
class _StreamedBytes:
|
||||
"""Raw tensor bytes produced chunk by chunk at write time (an Engram table is ~200 GB in BF16)."""
|
||||
def __init__(self, shape: tuple[int, ...], chunks: Callable[[], Iterable[np.ndarray]]):
|
||||
self.shape = shape
|
||||
self.dtype = np.dtype(np.uint8)
|
||||
self.nbytes = int(np.prod(shape))
|
||||
self._chunks = chunks
|
||||
|
||||
def tofile(self, fout) -> None:
|
||||
written = 0
|
||||
for chunk in self._chunks():
|
||||
chunk = np.ascontiguousarray(chunk, dtype=np.uint8)
|
||||
chunk.tofile(fout)
|
||||
written += chunk.nbytes
|
||||
if written != self.nbytes:
|
||||
raise ValueError(f"streamed {written} bytes, expected {self.nbytes}")
|
||||
|
||||
def byteswap(self, inplace: bool = False):
|
||||
raise NotImplementedError("streamed tensors are written little-endian only")
|
||||
|
||||
|
||||
def _e8m0_check(scale_u8: np.ndarray, name: str) -> None:
|
||||
# e4m3 (|x| in [2^-9, 448]) * 2^(e - 127) is exact in BF16 for e in [3, 246]
|
||||
if scale_u8.size and (int(scale_u8.min()) < 3 or int(scale_u8.max()) > 246):
|
||||
raise ValueError(f"{name}: E8M0 scale exponents outside [3, 246], the dequantized values would not be exact")
|
||||
|
||||
|
||||
@ModelBase.register("DeepseekV41ForCausalLM")
|
||||
class DeepseekV41Model(DeepseekV4Model):
|
||||
"""DeepSeek-V4.1 text model, lossless: FP8 (e4m3, 32x32 ue8m0 blocks) linears dequantized to BF16 (F32 with
|
||||
--outtype f32), routed experts copied bit-exact as MXFP4, Engram tables (e4m3 rows, one e8m0 scale per 32) as the
|
||||
exact BF16 lookup (F32 with --outtype f32), router and Engram gate F32. DSpark (mtp.*) is not converted here
|
||||
(--dspark, DeepseekV41DSparkModel); the vision tower goes to an mmproj (--mmproj, DeepseekV41VisionModel).
|
||||
The Engram tables of all layers are written as one per_layer_token_embd (rows concatenated in layer order) with
|
||||
the PLE keys of qwen4exp: absolute head offsets and bucket counts, so the runtime can read it lazily."""
|
||||
model_arch = gguf.MODEL_ARCH.DEEPSEEK41
|
||||
supports_mtp_export = False
|
||||
|
||||
_ENGRAM_EMBED_RE = re.compile(r"layers\.(\d+)\.engram\.embed\.(weight|scale)$")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.hparams.setdefault("num_hash_layers", 0) # no hash-routed layers in V4.1
|
||||
self._f32 = self.ftype == gguf.LlamaFileType.ALL_F32
|
||||
qc = self.hparams.get("quantization_config") or {}
|
||||
self._fp8_block = tuple(qc.get("weight_block_size") or (32, 32))
|
||||
if qc.get("scale_fmt", "ue8m0") != "ue8m0":
|
||||
raise ValueError(f"unsupported FP8 scale format {qc.get('scale_fmt')!r}")
|
||||
if self.hparams.get("gate_temp", 1.0) != 1.0:
|
||||
raise ValueError("gate_temp != 1 is not supported (the router logits would need dividing by it)")
|
||||
|
||||
# V4.1 does not use the DeepSeek-V4 template; a template shipped with the model is still added by set_vocab
|
||||
self.gguf_writer.remove_key(gguf.Keys.Tokenizer.CHAT_TEMPLATE)
|
||||
|
||||
def index_tensors(self, remote_hf_model_id: str | None = None) -> dict[str, Callable[[], Tensor]]:
|
||||
# num_hidden_layers lives in text_config, which is merged only after indexing; MTP stages are skipped anyway
|
||||
return TextModel.index_tensors(self, remote_hf_model_id=remote_hf_model_id)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
hp = self.hparams
|
||||
w = self.gguf_writer
|
||||
w.add_attention_kv_source_layers(hp["kv_source_layer_ids"])
|
||||
w.add_indexer_source_layers(hp["index_source_layer_ids"])
|
||||
if hp.get("candidate_source_layer_id", -1) >= 0:
|
||||
w.add_indexer_candidate_source_layer(hp["candidate_source_layer_id"])
|
||||
w.add_indexer_candidate_top_k_blocks(hp["candidate_topk_blocks"])
|
||||
w.add_indexer_candidate_block_size(hp["candidate_block_size"])
|
||||
|
||||
# GGUF cannot hold empty arrays: the Engram keys are omitted when the model has no Engram layers
|
||||
layer_ids = list(hp.get("engram_layer_ids") or [])
|
||||
if layer_ids:
|
||||
max_ngram, n_heads = hp["engram_max_ngram_size"], hp["engram_n_heads"]
|
||||
primes = engram_primes(len(layer_ids), max_ngram, n_heads, hp["engram_vocab_size"])
|
||||
for li, layer_primes in enumerate(primes):
|
||||
if sum(p for per in layer_primes for p in per) != hp["engram_num_embeddings"][li]:
|
||||
raise ValueError(f"engram layer {layer_ids[li]}: prime sum != num_embeddings")
|
||||
token_map, cvocab = engram_compressed_token_map(self.dir_model)
|
||||
if cvocab != hp["engram_compressed_vocab_size"] or len(token_map) != hp["vocab_size"]:
|
||||
raise ValueError(f"compressed token map ({len(token_map)} -> {cvocab}) does not match the config")
|
||||
# absolute row offsets into the concatenated table
|
||||
sizes = [p for layer in primes for per in layer for p in per]
|
||||
offsets = [sum(sizes[:i]) for i in range(len(sizes))]
|
||||
w.add_key_value(gguf.Keys.PerLayerEmbedding.LAYERS.format(arch=w.arch), layer_ids,
|
||||
gguf.GGUFValueType.ARRAY, gguf.GGUFValueType.UINT32)
|
||||
w.add_ple_ngram_size(max_ngram)
|
||||
w.add_ple_heads_per_ngram(n_heads)
|
||||
w.add_embedding_length_per_layer_input(hp["engram_head_dim"])
|
||||
w.add_ple_layer_multipliers([m for row in engram_multipliers(layer_ids, max_ngram, cvocab) for m in row])
|
||||
w.add_ple_head_offsets(offsets)
|
||||
w.add_ple_head_vocab_sizes(sizes)
|
||||
w.add_engram_pad_token_id(hp["engram_pad_token_id"])
|
||||
w.add_engram_token_map(token_map)
|
||||
|
||||
def dequant_model(self):
|
||||
bo, bi = self._fp8_block
|
||||
|
||||
def dequant(weight: Tensor, scale: Tensor) -> Tensor:
|
||||
out_f, in_f = weight.shape
|
||||
s = self._e8m0_to_float(scale).repeat_interleave(bo, 0)[:out_f].repeat_interleave(bi, 1)[:, :in_f]
|
||||
return weight.float() * s
|
||||
|
||||
for name in [n for n in self.model_tensors if n.endswith(".scale") and not self._ENGRAM_EMBED_RE.match(n)]:
|
||||
weight_name = name.removesuffix(".scale") + ".weight"
|
||||
if weight_name not in self.model_tensors or self.model_tensors[weight_name]().dtype not in self._float8_dtypes():
|
||||
continue
|
||||
w, s = self.model_tensors[weight_name], self.model_tensors.pop(name)
|
||||
out_f, in_f = w().shape
|
||||
if tuple(s().shape) != (-(-out_f // bo), -(-in_f // bi)):
|
||||
raise ValueError(f"{weight_name}: scale shape {tuple(s().shape)} does not match {(bo, bi)} blocks")
|
||||
_e8m0_check(LazyTorchTensor.to_eager(s()).view(torch.uint8).numpy(), name)
|
||||
self.model_tensors[weight_name] = lambda w=w, s=s: dequant(w(), s())
|
||||
self._dsv4_fp8_dequantized.add(weight_name)
|
||||
|
||||
def _tensor_part(self, name: str) -> Path:
|
||||
index = self.dir_model / "model.safetensors.index.json"
|
||||
if not index.is_file():
|
||||
return self.dir_model / "model.safetensors"
|
||||
with open(index, "r", encoding="utf-8") as f:
|
||||
return self.dir_model / json.load(f)["weight_map"][name]
|
||||
|
||||
def _write_mxfp4_experts(self, bid: int, proj: str, key: gguf.MODEL_TENSOR, n_expert: int | None = None) -> None:
|
||||
n_expert = self.hparams["n_routed_experts"] if n_expert is None else n_expert
|
||||
names = [f"layers.{bid}.ffn.experts.{e}.{proj}" for e in range(n_expert)]
|
||||
pairs = [(self.model_tensors.pop(n + ".weight"), self.model_tensors.pop(n + ".scale")) for n in names]
|
||||
rows, packed_cols = pairs[0][0]().shape
|
||||
|
||||
def chunks() -> Iterable[np.ndarray]:
|
||||
for w, s in pairs:
|
||||
yield self.repack_mxfp4_blocks(LazyTorchTensor.to_eager(w()), LazyTorchTensor.to_eager(s()))
|
||||
data = cast(np.ndarray, _StreamedBytes((len(pairs), rows, packed_cols * 2 // 32 * 17), chunks))
|
||||
self.gguf_writer.add_tensor(self.format_tensor_name(key, bid), data, raw_dtype=gguf.GGMLQuantizationType.MXFP4)
|
||||
|
||||
def _write_engram_tables(self, layer_ids: list[int]) -> None:
|
||||
# official lookup e4m3 * 2^(e8m0 - 127), exact in BF16
|
||||
parts, dim = [], None
|
||||
for bid in layer_ids:
|
||||
wn, sn = f"layers.{bid}.engram.embed.weight", f"layers.{bid}.engram.embed.scale"
|
||||
rows, d = self.model_tensors.pop(wn)().shape
|
||||
if d % 32 != 0 or tuple(self.model_tensors.pop(sn)().shape) != (rows, d // 32) or d != (dim or d):
|
||||
raise ValueError(f"{wn}: expected one e8m0 scale per 32 elements and the same width in every layer")
|
||||
if rows != self.hparams["engram_num_embeddings"][len(parts)]:
|
||||
raise ValueError(f"{wn}: {rows} rows, config says {self.hparams['engram_num_embeddings'][len(parts)]}")
|
||||
dim = d
|
||||
parts.append((wn, sn, rows, self._tensor_part(wn), self._tensor_part(sn)))
|
||||
assert dim is not None
|
||||
name = self.format_tensor_name(gguf.MODEL_TENSOR.PER_LAYER_TOKEN_EMBD)
|
||||
n_rows = sum(p[2] for p in parts)
|
||||
|
||||
def chunks() -> Iterable[np.ndarray]:
|
||||
from safetensors import safe_open
|
||||
for wn, sn, rows, wpart, spart in parts:
|
||||
with safe_open(wpart, framework="pt") as fw, safe_open(spart, framework="pt") as fs:
|
||||
ws, ss = fw.get_slice(wn), fs.get_slice(sn)
|
||||
for r0 in range(0, rows, 1 << 18):
|
||||
q = ws[r0:r0 + (1 << 18)].view(torch.uint8).reshape(-1, 32)
|
||||
e = ss[r0:r0 + (1 << 18)].view(torch.uint8).reshape(-1, 1)
|
||||
_e8m0_check(e.numpy(), sn)
|
||||
v = (q.view(torch.float8_e4m3fn).float() * torch.exp2(e.float() - 127)).reshape(-1, dim)
|
||||
yield (v if self._f32 else v.bfloat16()).view(torch.uint8).numpy()
|
||||
qt = gguf.GGMLQuantizationType.F32 if self._f32 else gguf.GGMLQuantizationType.BF16
|
||||
data = cast(np.ndarray, _StreamedBytes((n_rows, dim * (4 if self._f32 else 2)), chunks))
|
||||
self.gguf_writer.add_tensor(name, data, raw_dtype=qt)
|
||||
|
||||
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
||||
if not self._dsv4_mxfp4_generated:
|
||||
for bid in range(self.block_count):
|
||||
self._write_mxfp4_experts(bid, "w1", gguf.MODEL_TENSOR.FFN_GATE_EXP)
|
||||
self._write_mxfp4_experts(bid, "w2", gguf.MODEL_TENSOR.FFN_DOWN_EXP)
|
||||
self._write_mxfp4_experts(bid, "w3", gguf.MODEL_TENSOR.FFN_UP_EXP)
|
||||
if self.hparams.get("engram_layer_ids"):
|
||||
self._write_engram_tables(list(self.hparams["engram_layer_ids"]))
|
||||
self._dsv4_mxfp4_generated = True
|
||||
return ()
|
||||
|
||||
_LAYER_MAP: dict[str, tuple[gguf.MODEL_TENSOR, str]] = {
|
||||
"attn.indexer.wk.weight": (gguf.MODEL_TENSOR.INDEXER_ATTN_K, ".weight"),
|
||||
"attn.indexer.k_norm.weight": (gguf.MODEL_TENSOR.INDEXER_K_NORM, ".weight"),
|
||||
"engram.wkv.weight": (gguf.MODEL_TENSOR.ENGRAM_KV, ".weight"),
|
||||
}
|
||||
|
||||
def _map_dsv4_tensor_name(self, name: str, bid: int | None) -> tuple[gguf.MODEL_TENSOR, str]:
|
||||
m = re.match(r"layers\.\d+\.(.+)$", name)
|
||||
if m is not None and m.group(1) in self._LAYER_MAP:
|
||||
return self._LAYER_MAP[m.group(1)]
|
||||
return super()._map_dsv4_tensor_name(name, bid)
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name.endswith(".engram.k_weight"):
|
||||
return # folded into engram_gate
|
||||
if name.endswith(".engram.q_weight"):
|
||||
# Engram gate weight = q_weight * k_weight (bf16 * bf16 is exact in f32)
|
||||
k = self.model_tensors[name.removesuffix("q_weight") + "k_weight"]
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.ENGRAM_GATE, bid), data_torch.float() * k().float()
|
||||
return
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
def tensor_force_quant(self, name: str, new_name: str, bid: int | None, n_dims: int) -> gguf.GGMLQuantizationType | bool:
|
||||
if new_name.endswith((".engram_gate.weight", ".ffn_gate_inp.weight")) or name in self._dsv4_f32_tensors:
|
||||
return gguf.GGMLQuantizationType.F32
|
||||
if n_dims >= 2 and (name in self._dsv4_fp8_dequantized or name in self._dsv4_bf16_tensors):
|
||||
return gguf.GGMLQuantizationType.F32 if self._f32 else gguf.GGMLQuantizationType.BF16
|
||||
return False
|
||||
|
||||
|
||||
class DeepseekV41DSparkModel(DeepseekV41Model):
|
||||
"""DeepSeek-V4.1 DSpark speculative draft (mtp.0..2 of the checkpoint) as a DFLASH GGUF (`--dspark`).
|
||||
|
||||
Layout = the V4 DSpark draft (conversion/deepseek.py DeepseekV4DSparkModel) with V4.1's differences flagged by
|
||||
`dflash.hyper_connection.single_pass`: stages blk.0..2 (MXFP4 experts bit-exact, dense BF16), mtp.0.main_proj /
|
||||
main_norm -> fc / enc.output_norm, mtp.2.norm -> output_norm, markov_head.embed / head -> markov_w1 / markov_w2,
|
||||
confidence_head.proj -> conf_proj (F32), no hc_head. Token embeddings and the lm head are the target's (shared at
|
||||
runtime through ctx_other). target_layers are the V4.1 ids as-is: the draft reads the attention input of those
|
||||
layers, which is what llama_set_embeddings_layer_inp extracts.
|
||||
"""
|
||||
model_arch = gguf.MODEL_ARCH.DFLASH
|
||||
mtp_only = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
hp = self.hparams
|
||||
self._n_main = hp["num_hidden_layers"]
|
||||
self._n_stage = hp["num_nextn_predict_layers"]
|
||||
if self._n_stage <= 0 or "dspark_block_size" not in hp:
|
||||
raise ValueError("DeepSeek-V4.1 DSpark export needs num_nextn_predict_layers > 0 and the dspark_* config")
|
||||
# stages keep main-model block ids (mtp.M -> layers.n_main+M); format_tensor_name() maps them back
|
||||
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self._n_stage)
|
||||
|
||||
def index_tensors(self, remote_hf_model_id: str | None = None) -> dict[str, Callable[[], Tensor]]:
|
||||
hp = {**self.hparams, **self.hparams.get("text_config", {})} # text_config is merged only after indexing
|
||||
type(self)._dsv4_main_layers = hp["num_hidden_layers"]
|
||||
type(self)._dsv4_nextn_layers = hp.get("num_nextn_predict_layers", 0)
|
||||
return TextModel.index_tensors(self, remote_hf_model_id=remote_hf_model_id)
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
name, gen = item
|
||||
m = re.match(r"mtp\.(\d+)\.(.+)$", name)
|
||||
if m is None:
|
||||
return None # shared with the target at runtime
|
||||
stage, rest = int(m.group(1)), m.group(2)
|
||||
assert cls._dsv4_main_layers is not None
|
||||
if stage >= cls._dsv4_nextn_layers:
|
||||
raise ValueError(f"Unexpected DSpark stage {stage}")
|
||||
if rest.startswith(("markov_head.", "confidence_head.")):
|
||||
return f"dspark.{rest}", gen
|
||||
return f"layers.{cls._dsv4_main_layers + stage}.{rest}", gen
|
||||
|
||||
def format_tensor_name(self, key: gguf.MODEL_TENSOR, bid: int | None = None, suffix: str = ".weight") -> str:
|
||||
if bid is not None and bid >= self._n_main:
|
||||
bid -= self._n_main
|
||||
return super().format_tensor_name(key, bid, suffix)
|
||||
|
||||
def set_vocab(self):
|
||||
super().set_vocab()
|
||||
self.gguf_writer.add_mask_token_id(self.hparams["dspark_noise_token_id"])
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
hp = self.hparams
|
||||
n = self._n_stage
|
||||
w = self.gguf_writer
|
||||
w.add_block_count(n)
|
||||
w.add_context_length(hp["max_position_embeddings"])
|
||||
w.add_embedding_length(hp["hidden_size"])
|
||||
w.add_feed_forward_length(hp["moe_intermediate_size"])
|
||||
w.add_head_count(hp["num_attention_heads"])
|
||||
w.add_head_count_kv(1)
|
||||
w.add_key_length(hp["head_dim"])
|
||||
w.add_value_length(hp["head_dim"])
|
||||
w.add_rope_dimension_count(hp["qk_rope_head_dim"])
|
||||
# compress-ratio-0 layers: plain rope at rope_theta, no YaRN
|
||||
w.add_rope_freq_base(float(hp["rope_theta"]))
|
||||
w.add_layer_norm_rms_eps(hp["rms_norm_eps"])
|
||||
w.add_q_lora_rank(hp["q_lora_rank"])
|
||||
w.add_sliding_window(hp["sliding_window"])
|
||||
w.add_expert_count(hp["dspark_n_routed_experts"])
|
||||
w.add_expert_used_count(hp["dspark_num_experts_per_tok"])
|
||||
w.add_expert_feed_forward_length(hp["moe_intermediate_size"])
|
||||
w.add_expert_shared_count(hp["n_shared_experts"])
|
||||
w.add_expert_weights_scale(hp["routed_scaling_factor"])
|
||||
w.add_expert_weights_norm(hp["norm_topk_prob"])
|
||||
w.add_expert_gating_func(gguf.ExpertGatingFuncType.SQRTSOFTPLUS)
|
||||
w.add_swiglu_clamp_exp([hp["swiglu_limit"]] * n)
|
||||
w.add_swiglu_clamp_shexp([hp["swiglu_limit"]] * n)
|
||||
w.add_attention_output_group_count(hp["o_groups"])
|
||||
w.add_attention_output_lora_rank(hp["o_lora_rank"])
|
||||
w.add_attention_compress_ratios([0] * n)
|
||||
w.add_hyper_connection_count(hp["hc_mult"])
|
||||
w.add_hyper_connection_sinkhorn_iterations(hp["hc_sinkhorn_iters"])
|
||||
w.add_hyper_connection_epsilon(hp["hc_eps"])
|
||||
w.add_hyper_connection_single_pass(True)
|
||||
w.add_block_size(hp["dspark_block_size"])
|
||||
w.add_target_layers(hp["dspark_target_layer_ids"])
|
||||
w.add_sample_from_anchor(True)
|
||||
w.add_has_confidence_head(True)
|
||||
w.add_file_type(self.ftype)
|
||||
|
||||
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
||||
# no Engram tables in the draft
|
||||
if not self._dsv4_mxfp4_generated:
|
||||
n_expert = self.hparams["dspark_n_routed_experts"]
|
||||
for bid in range(self._n_main, self._n_main + self._n_stage):
|
||||
self._write_mxfp4_experts(bid, "w1", gguf.MODEL_TENSOR.FFN_GATE_EXP, n_expert)
|
||||
self._write_mxfp4_experts(bid, "w2", gguf.MODEL_TENSOR.FFN_DOWN_EXP, n_expert)
|
||||
self._write_mxfp4_experts(bid, "w3", gguf.MODEL_TENSOR.FFN_UP_EXP, n_expert)
|
||||
self._dsv4_mxfp4_generated = True
|
||||
return ()
|
||||
|
||||
_DSPARK_ROOT: dict[str, gguf.MODEL_TENSOR] = {
|
||||
"main_proj.weight": gguf.MODEL_TENSOR.FC,
|
||||
"main_norm.weight": gguf.MODEL_TENSOR.ENC_OUTPUT_NORM,
|
||||
"norm.weight": gguf.MODEL_TENSOR.OUTPUT_NORM,
|
||||
"dspark.markov_head.embed.weight": gguf.MODEL_TENSOR.DSPARK_MARKOV_W1,
|
||||
"dspark.markov_head.head.weight": gguf.MODEL_TENSOR.DSPARK_MARKOV_W2,
|
||||
"dspark.confidence_head.proj.weight": gguf.MODEL_TENSOR.DSPARK_CONF_PROJ,
|
||||
}
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name.endswith(".ffn.gate.bias_vl"):
|
||||
return
|
||||
if name in self._DSPARK_ROOT:
|
||||
yield self.format_tensor_name(self._DSPARK_ROOT[name], None), data_torch
|
||||
return
|
||||
m = re.match(r"layers\.\d+\.(main_proj\.weight|main_norm\.weight|norm\.weight)$", name)
|
||||
if m is not None:
|
||||
yield self.format_tensor_name(self._DSPARK_ROOT[m.group(1)], None), data_torch
|
||||
return
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
def tensor_force_quant(self, name: str, new_name: str, bid: int | None, n_dims: int) -> gguf.GGMLQuantizationType | bool:
|
||||
if new_name == "conf_proj.weight":
|
||||
return gguf.GGMLQuantizationType.F32
|
||||
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
||||
|
||||
|
||||
@ModelBase.register("DeepseekV41ForCausalLM")
|
||||
@ModelBase.example("deepseek-ai/DeepSeek-V4.1-Flash-1017")
|
||||
class DeepseekV41VisionModel(DeepseekV4FlashVisionModel):
|
||||
"""DeepSeek-V4.1 vision tower (projector deepseek41v): the V4-Flash-Vision ViT + 3x3 aligner, with the V4.1 token
|
||||
block ([START] ([IMAGE]*w [NEWLINE])*h [END], no pads) and resize plan. ref: inference/vision.py,
|
||||
inference/image_processor.py of deepseek-ai/DeepSeek-V4.1-Flash-1017. The checkpoint config is HF-style
|
||||
(vision_config); image_start / image_end / image_newline are written like V4's sentinels, there is no image_pad."""
|
||||
|
||||
def get_vision_config(self) -> dict[str, Any] | None:
|
||||
vc = self.global_config.get("vision_config")
|
||||
if not vc or vc.get("num_hidden_layers", 0) == 0:
|
||||
raise ValueError("DeepseekV41VisionModel requires vision_config.num_hidden_layers > 0")
|
||||
return {
|
||||
"num_hidden_layers": vc["num_hidden_layers"],
|
||||
"hidden_size": vc["hidden_size"],
|
||||
"num_attention_heads": vc["num_attention_heads"],
|
||||
"intermediate_size": vc["intermediate_size"],
|
||||
"patch_size": vc["patch_size"],
|
||||
# dynamic resolution; only used for compat / warmup
|
||||
"image_size": vc["patch_size"] * vc["downsample_ratio"] * 16,
|
||||
"rope_theta": vc.get("rope_theta", 10000.0),
|
||||
"downsample_ratio": vc["downsample_ratio"],
|
||||
"min_pixels": vc["min_pixels"],
|
||||
"max_image_tokens": vc["max_image_tokens"],
|
||||
"max_wh_ratio": vc.get("max_wh_ratio"),
|
||||
}
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
assert self.hparams_vision is not None
|
||||
super(DeepseekV4FlashVisionModel, self).set_gguf_parameters()
|
||||
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.DEEPSEEK41V)
|
||||
# pytorch default eps (vision.py), not the LLM rms_norm_eps (1e-20)
|
||||
self.gguf_writer.add_vision_attention_layernorm_eps(1e-6)
|
||||
self.gguf_writer.add_vision_use_silu(True)
|
||||
self.gguf_writer.add_vision_projector_scale_factor(self.hparams_vision["downsample_ratio"])
|
||||
self.gguf_writer.add_vision_min_pixels(self.hparams_vision["min_pixels"])
|
||||
# hardcoded in clip.cpp (PROJECTOR_TYPE_DEEPSEEK41V); add GGUF keys if a later model differs
|
||||
assert self.hparams_vision["max_image_tokens"] == 1024, self.hparams_vision["max_image_tokens"]
|
||||
assert self.hparams_vision["max_wh_ratio"] is None, self.hparams_vision["max_wh_ratio"]
|
||||
assert self.hparams_vision["rope_theta"] == 10000, self.hparams_vision["rope_theta"]
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import ModelBase, gguf
|
||||
from .qwen import Qwen2MoeModel
|
||||
|
||||
|
||||
@ModelBase.register("FlintForCausalLM")
|
||||
@ModelBase.example("RecursiveSI/flint")
|
||||
class FlintModel(Qwen2MoeModel):
|
||||
model_arch = gguf.MODEL_ARCH.FLINT
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
dense = self.hparams["mlp_only_layers"]
|
||||
if dense != list(range(len(dense))):
|
||||
raise ValueError("Flint dense layers must precede the MoE layers")
|
||||
self.gguf_writer.add_leading_dense_block_count(len(dense))
|
||||
self.gguf_writer.add_expert_shared_count(
|
||||
self.hparams["shared_expert_intermediate_size"] // self.hparams["moe_intermediate_size"]
|
||||
)
|
||||
self.gguf_writer.add_expert_weights_norm(self.hparams["norm_topk_prob"])
|
||||
self.gguf_writer.add_expert_weights_scale(1.0)
|
||||
self.gguf_writer.add_sliding_window(self.hparams["sliding_window"])
|
||||
self.gguf_writer.add_sliding_window_pattern([
|
||||
kind == "sliding_attention" for kind in self.hparams["layer_types"]
|
||||
])
|
||||
self.gguf_writer.add_rope_pattern([
|
||||
i not in self.hparams["no_rope_layer_indices"] for i in range(self.block_count)
|
||||
])
|
||||
|
||||
def modify_tensors(self, data_torch, name, bid):
|
||||
if name.endswith(".softmax_offset"):
|
||||
name += ".weight"
|
||||
if name.endswith(".gate.expert_bias"):
|
||||
if not self.hparams["router_expert_bias"]:
|
||||
return
|
||||
name += ".bias"
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
@@ -999,6 +999,8 @@ class Gemma4VisionAudioModel(MmprojModel):
|
||||
assert self.hparams_vision is not None
|
||||
self.gguf_writer.add_clip_vision_projector_type(gguf.VisionProjectorType.GEMMA4V)
|
||||
self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams_vision.get("layer_norm_eps", 1e-6))
|
||||
assert self.hparams_vision.get("hidden_activation", "gelu_pytorch_tanh") == "gelu_pytorch_tanh"
|
||||
self.gguf_writer.add_vision_use_gelu(True)
|
||||
|
||||
# audio params
|
||||
if self.has_audio_encoder:
|
||||
|
||||
+46
-3
@@ -8,7 +8,7 @@ import torch
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import MmprojModel, ModelBase, gguf, logger
|
||||
from .base import MmprojModel, ModelBase, gguf, jinja_str_or_json, logger
|
||||
|
||||
from .llama import LlamaModel
|
||||
from .mamba import Mamba2Model
|
||||
@@ -239,8 +239,21 @@ class GraniteSwitchModel(GraniteMoeModel):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# the weightless switch reserves one cache slot: one fewer block than num_hidden_layers
|
||||
self.block_count = self.block_count - 1
|
||||
# The weightless switch layer(s) are counted in num_hidden_layers but carry no tensors.
|
||||
# The count is not fixed, nor is it present in hparams, so we derive it here based on tensor block names.
|
||||
layer_ids = {
|
||||
int(m.group(1))
|
||||
for name in self.model_tensors
|
||||
if (m := re.match(r"(?:model\.)?layers\.(\d+)\.", name))
|
||||
}
|
||||
if not layer_ids:
|
||||
raise ValueError("graniteswitch: no 'model.layers.N' tensors found in checkpoint")
|
||||
n_weight_layers = max(layer_ids) + 1
|
||||
logger.info(
|
||||
"gguf: (graniteswitch) num_hidden_layers %s counts %d weightless switch layer(s); using %d blocks",
|
||||
self.hparams["num_hidden_layers"], self.block_count - n_weight_layers, n_weight_layers,
|
||||
)
|
||||
self.block_count = n_weight_layers
|
||||
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
||||
|
||||
self._n_adapters = int(self.hparams["num_adapters"])
|
||||
@@ -271,10 +284,40 @@ class GraniteSwitchModel(GraniteMoeModel):
|
||||
self.gguf_writer.add_adapter_lora_rank(self._max_lora_rank)
|
||||
self.gguf_writer.add_adapter_token_ids_activate(self.hparams["adapter_token_ids"])
|
||||
self.gguf_writer.add_adapter_token_ids_substitute(self.hparams["adapter_substitute_token_ids"])
|
||||
if self._has_decision_adapter():
|
||||
self.gguf_writer.add_decision_type(gguf.DecisionType.GRANITE_SWITCH)
|
||||
router_gain = float(self.hparams.get("control_token_gain", 15.0))
|
||||
self.gguf_writer.add_adapter_router_gain(router_gain)
|
||||
logger.info("gguf: (graniteswitch) num_adapters=%s max_lora_rank=%s n_slots=%s router_gain=%s", self._n_adapters, self._max_lora_rank, self._n_slots, router_gain)
|
||||
|
||||
def _has_decision_adapter(self) -> bool:
|
||||
return "decision" in self.hparams.get("adapter_names", [])
|
||||
|
||||
def set_vocab(self):
|
||||
super().set_vocab()
|
||||
if self._has_decision_adapter():
|
||||
systemone_template = self._systemone_template()
|
||||
logger.info("System One Template: %s", systemone_template)
|
||||
self.gguf_writer.add_chat_template([{"name": "systemone", "template": systemone_template}])
|
||||
|
||||
def _systemone_template(self) -> str:
|
||||
# prompt of the decision aLoRA: a state turn, then a question turn that opens with the control token
|
||||
# (it replaces the first token of "### DECISION"), the answer is the label token that comes next
|
||||
description = jinja_str_or_json("o.description")
|
||||
option = (
|
||||
"{% if type != 'noul' %}{{ o.label }}: {% if o.description %}" + description + "{% endif %}"
|
||||
"{% elif o.key == 'true' %}A: {% if o.description %}" + description + "{% else %}The statement is true.{% endif %}"
|
||||
"{% else %}B: {% if o.description %}" + description + "{% else %}The statement is false.{% endif %}{% endif %}"
|
||||
)
|
||||
return (
|
||||
"<|start_of_role|>user<|end_of_role|>State:\n" + jinja_str_or_json("state") + "<|end_of_text|>\n"
|
||||
"<|start_of_role|>user<|end_of_role|><|decision|> DECISION\nQuestion:\n" + jinja_str_or_json("instructions")
|
||||
+ "{% if type == 'score' %} Rate along the ordered levels below (lowest first).{% endif %}"
|
||||
"{{ '\n\nOptions:\n' }}"
|
||||
"{% for o in options %}" + option + "{{ '\n' }}{% endfor %}"
|
||||
"{{ '\nReply with exactly one option label.<|end_of_text|>\n<|start_of_role|>assistant<|end_of_role|>' }}"
|
||||
)
|
||||
|
||||
def _lora_a(self, data: Tensor) -> Tensor:
|
||||
# on-disk A: [n_adapters, 1, max_rank, in] -> [n_adapters+1, max_rank, in]
|
||||
a = data.squeeze(1)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .base import ModelBase, gguf, logger
|
||||
from .qwen import Qwen3_5TextModel
|
||||
from .qwen3vl import Qwen3VLVisionModel
|
||||
|
||||
|
||||
def _is_h2o_lightning_checkpoint(dir_model: Path) -> bool:
|
||||
return (dir_model / "h2o_lightning_shim.py").is_file() and (dir_model / "serve_config.json").is_file()
|
||||
|
||||
|
||||
@ModelBase.register_hparams_loader(_is_h2o_lightning_checkpoint)
|
||||
def _load_h2o_lightning_hparams(dir_model: Path) -> dict[str, Any]:
|
||||
logger.info("gguf: detected H2O-Lightning checkpoint")
|
||||
hparams = ModelBase.load_hparams(dir_model, False, guess=False)
|
||||
hparams["architectures"] = ["H2OLightningModel"]
|
||||
return hparams
|
||||
|
||||
|
||||
@ModelBase.register("H2OLightningModel")
|
||||
@ModelBase.example("h2oai/h2o-lightning-4b")
|
||||
class H2OLightningModel(Qwen3_5TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.QWEN35
|
||||
|
||||
# prompt and calibration follow serve_config.json and h2o_lightning_shim.py of the model repo
|
||||
# the state budget is hard-coded in server-decision.cpp
|
||||
_MAX_STATE_TOKENS = 32000
|
||||
_DEFAULT_INSTRUCTIONS = "Answer the question below."
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
with open(self.dir_model / "serve_config.json", "r", encoding="utf-8") as f:
|
||||
self._serve_config = json.load(f)
|
||||
prompt = self._serve_config["prompt"]
|
||||
expected = {
|
||||
"state_compact": (prompt.get("state_compact", True), True),
|
||||
"max_state_tokens": (prompt.get("max_state_tokens"), self._MAX_STATE_TOKENS),
|
||||
"layout": (prompt.get("layout", "plain"), "plain"),
|
||||
"chat_template": (prompt.get("chat_template", True), True),
|
||||
"enable_thinking": (prompt.get("enable_thinking", False), False),
|
||||
}
|
||||
for name, (got, want) in expected.items():
|
||||
if got != want:
|
||||
raise ValueError(f"serve_config.json: {name} is {got!r}, the h2o-lightning decision type expects {want!r}")
|
||||
|
||||
def get_vocab_base_pre(self, tokenizer) -> str:
|
||||
# transformers loads this checkpoint as Qwen2Tokenizer: the model was trained and is served with the Qwen2 split
|
||||
pre = super().get_vocab_base_pre(tokenizer)
|
||||
if pre != "qwen35":
|
||||
raise ValueError(f"expected the Qwen3.5 tokenizer, got pre-tokenizer {pre!r}")
|
||||
return "qwen2"
|
||||
|
||||
def set_vocab(self):
|
||||
super().set_vocab()
|
||||
self.gguf_writer.add_chat_template([{"name": "systemone", "template": self._systemone_template()}])
|
||||
|
||||
def _systemone_template(self) -> str:
|
||||
# the server gives the template texts rendered as the shim renders them, it only lays them out
|
||||
prompt = self._serve_config["prompt"]
|
||||
nl = "{{ '\\n' }}"
|
||||
description = (
|
||||
"{% if type == 'noul' %}"
|
||||
"{% if o.description %}{{ o.description }}{% elif o.key == 'true' %}the statement holds{% else %}it does not{% endif %}"
|
||||
"{% elif o.description is none %}{{ o.key }}{% else %}{{ o.description }}{% endif %}"
|
||||
)
|
||||
user = (
|
||||
"{% set user %}record: {{ state }}" + nl
|
||||
+ "question: {{ instructions if instructions else '" + self._DEFAULT_INSTRUCTIONS + "' }}" + nl
|
||||
+ "options:{% for o in options %}" + nl + "{{ o.label | trim }}) {{ o.key }}: " + description + "{% endfor %}{% endset %}"
|
||||
)
|
||||
return (
|
||||
"<|im_start|>system" + nl + prompt["system"].strip() + "<|im_end|>" + nl
|
||||
+ "<|im_start|>user" + nl + "{% for image in images %}{{ image }}{% endfor %}" + user + "{{ user | trim }}<|im_end|>" + nl
|
||||
+ "<|im_start|>assistant" + nl + "<think>{{ '\\n\\n' }}</think>{{ '\\n\\n' }}" + prompt["prefill"]
|
||||
)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_decision_type(gguf.DecisionType.H2O_LIGHTNING)
|
||||
# the answer follows "Answer:", so the token of a label has a leading space
|
||||
cfg = self._serve_config
|
||||
self.gguf_writer.add_decision_labels([" " + label for label in cfg["labels"][:cfg.get("max_options", len(cfg["labels"]))]])
|
||||
for name in ("choice", "noul", "score"):
|
||||
self.gguf_writer.add_decision_temperature(name, self._serve_config["temperature"][name])
|
||||
# a request with images has this temperature for all question types
|
||||
self.gguf_writer.add_decision_temperature("image", self._serve_config["image"]["temperature"])
|
||||
|
||||
|
||||
@ModelBase.register("H2OLightningModel")
|
||||
class H2OLightningVisionModel(Qwen3VLVisionModel):
|
||||
pass
|
||||
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .afmoe import AfmoeModel
|
||||
from .base import ModelBase, gguf
|
||||
|
||||
|
||||
@ModelBase.register("Kolibri1ForCausalLM")
|
||||
@ModelBase.example("Aleph-Alpha/Kolibri-1")
|
||||
class Kolibri1Model(AfmoeModel):
|
||||
model_arch = gguf.MODEL_ARCH.KOLIBRI1
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# AfmoeModel reads these keys, this config stores the same values under other names
|
||||
self.hparams["num_shared_experts"] = 1
|
||||
self.hparams["route_norm"] = self.hparams["norm_topk_prob"]
|
||||
self.hparams["intermediate_size"] = self.hparams["moe_intermediate_size"]
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_expert_shared_feed_forward_length(self.hparams["shared_expert_intermediate_size"])
|
||||
self.gguf_writer.add_sliding_window_pattern([t == "sliding_attention" for t in self.hparams["layer_types"]])
|
||||
self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SIGMOID)
|
||||
+31
-14
@@ -15,25 +15,30 @@ from .base import MmprojModel, ModelBase, TextModel, gguf, jinja_str_or_json, lo
|
||||
from .gemma import ConformerAudioModel
|
||||
|
||||
|
||||
def lfm2_feed_forward_length(hparams: dict[str, Any]) -> int:
|
||||
ff_dim = hparams.get("block_ff_dim", hparams.get("intermediate_size"))
|
||||
assert ff_dim is not None
|
||||
auto_adjust_ff_dim = hparams["block_auto_adjust_ff_dim"]
|
||||
ffn_dim_multiplier = hparams["block_ffn_dim_multiplier"]
|
||||
multiple_of = hparams["block_multiple_of"]
|
||||
|
||||
if auto_adjust_ff_dim:
|
||||
ff_dim = int(2 * ff_dim / 3)
|
||||
# custom dim factor multiplier
|
||||
if ffn_dim_multiplier is not None:
|
||||
ff_dim = int(ffn_dim_multiplier * ff_dim)
|
||||
ff_dim = multiple_of * ((ff_dim + multiple_of - 1) // multiple_of)
|
||||
|
||||
return ff_dim
|
||||
|
||||
|
||||
@ModelBase.register("Lfm2ForCausalLM", "LFM2ForCausalLM")
|
||||
@ModelBase.example("LiquidAI/LFM2-1.2B", "LiquidAI/LFM2.5-350M")
|
||||
class LFM2Model(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.LFM2
|
||||
|
||||
def _add_feed_forward_length(self):
|
||||
ff_dim = self.find_hparam(["block_ff_dim", "intermediate_size"])
|
||||
auto_adjust_ff_dim = self.hparams["block_auto_adjust_ff_dim"]
|
||||
ffn_dim_multiplier = self.hparams["block_ffn_dim_multiplier"]
|
||||
multiple_of = self.hparams["block_multiple_of"]
|
||||
|
||||
if auto_adjust_ff_dim:
|
||||
ff_dim = int(2 * ff_dim / 3)
|
||||
# custom dim factor multiplier
|
||||
if ffn_dim_multiplier is not None:
|
||||
ff_dim = int(ffn_dim_multiplier * ff_dim)
|
||||
ff_dim = multiple_of * ((ff_dim + multiple_of - 1) // multiple_of)
|
||||
|
||||
self.gguf_writer.add_feed_forward_length(ff_dim)
|
||||
self.gguf_writer.add_feed_forward_length(lfm2_feed_forward_length(self.hparams))
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
# set num_key_value_heads only for attention layers
|
||||
@@ -72,7 +77,9 @@ def _is_d1_checkpoint(dir_model: Path) -> bool:
|
||||
if not (dir_model / "config.json").is_file():
|
||||
return False
|
||||
with open(dir_model / "config.json", encoding="utf-8") as f:
|
||||
return json.load(f).get("auto_map", {}).get("AutoModel", "").endswith(".D1Model")
|
||||
config = json.load(f)
|
||||
# d1 on LFM3 is converted by conversion/lfm3.py
|
||||
return config.get("auto_map", {}).get("AutoModel", "").endswith(".D1Model") and not config.get("model_type", "").startswith("lfm3")
|
||||
|
||||
|
||||
@ModelBase.register_hparams_loader(_is_d1_checkpoint)
|
||||
@@ -374,6 +381,16 @@ class LFM2VLModel(MmprojModel):
|
||||
if resize_algo is None:
|
||||
raise ValueError(f"unsupported resample: {resample}")
|
||||
self.gguf_writer.add_vision_image_resize_algo(resize_algo)
|
||||
# tiling params from processor_config.json (do_image_splitting=False -> single tile, i.e. no tiling)
|
||||
preproc = self.preprocessor_config
|
||||
if preproc.get("do_image_splitting", True):
|
||||
self.gguf_writer.add_vision_preproc_min_tiles(preproc.get("min_tiles", 2))
|
||||
self.gguf_writer.add_vision_preproc_max_tiles(preproc.get("max_tiles", 10))
|
||||
else:
|
||||
self.gguf_writer.add_vision_preproc_min_tiles(1)
|
||||
self.gguf_writer.add_vision_preproc_max_tiles(1)
|
||||
self.gguf_writer.add_vision_preproc_tile_size(preproc.get("tile_size", 512))
|
||||
self.gguf_writer.add_vision_preproc_max_pixels_tolerance(preproc.get("max_pixels_tolerance", 2.0))
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
|
||||
@@ -0,0 +1,629 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Iterable, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import MmprojModel, ModelBase, TextModel, gguf, logger
|
||||
from .lfm2 import D1Model, lfm2_feed_forward_length
|
||||
|
||||
|
||||
@ModelBase.register("Lfm3ForCausalLM", "Lfm3VlForConditionalGeneration", "Lfm3OmniForConditionalGeneration")
|
||||
@ModelBase.example("LiquidAI/LFM3-1.7B", "LiquidAI/LFM3-1.7B-Base")
|
||||
class LFM3Model(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.LFM3
|
||||
|
||||
# LFM3-Audio/Omni: encoder, projector and audio-output heads live next to the text model
|
||||
_media_prefixes = ("encoder.", "mlp.", "codebook_embedding.", "depth_embeddings.", "depth_linear.", "depthformer.",
|
||||
"mask_embedding", "model.codebook_embedding.", "model.depth_decoder.")
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
name, gen = item
|
||||
|
||||
if name.startswith(cls._media_prefixes):
|
||||
return None
|
||||
|
||||
name = name.removeprefix("lfm.")
|
||||
|
||||
return super().filter_tensors((name, gen))
|
||||
|
||||
# LFM3-Audio live ASR prompt of the model card, the audio follows in the assistant turn
|
||||
_asr_template = (
|
||||
"{{- bos_token -}}"
|
||||
"{{- '<|im_start|>system\\nPerform live ASR.<|im_end|>\\n' -}}"
|
||||
"{%- if add_generation_prompt -%}{{- '<|im_start|>assistant\\n' -}}{%- endif -%}"
|
||||
)
|
||||
|
||||
# TTS prompt of the model card: a language code is looked up, anything else is taken as the language name
|
||||
_tts_langs = {
|
||||
"ar": "Arabic", "da": "Danish", "de": "German", "el": "Greek", "en": "American English", "en-us": "American English",
|
||||
"en-gb": "British English", "fi": "Finnish", "fr": "French", "he": "Hebrew", "it": "Italian", "ja": "Japanese",
|
||||
"ko": "Korean", "nl": "Dutch", "pl": "Polish", "pt": "Portuguese", "es": "Spanish", "sv": "Swedish", "tr": "Turkish",
|
||||
"zh": "Chinese",
|
||||
}
|
||||
|
||||
# "language" and "voice" are optional, the text to speak is the first message
|
||||
def _tts_template(self, think: bool) -> str:
|
||||
return (
|
||||
"{%- set names = " + json.dumps(self._tts_langs) + " -%}"
|
||||
"{%- set code = (language | lower) if language is defined and language else 'en' -%}"
|
||||
"{%- set lang = names[code] if code in names else language -%}"
|
||||
"{%- set v = (voice | lower) if voice is defined and voice and (voice | lower) in ['female', 'male'] else 'female' -%}"
|
||||
"{{- bos_token -}}"
|
||||
"{{- '<|im_start|>system\\nPerform TTS in ' + lang + ' using a ' + v + ' voice.<|im_end|>\\n' -}}"
|
||||
"{{- '<|im_start|>user\\n' + messages[0]['content'] + '<|im_end|>\\n' -}}"
|
||||
"{{- '<|im_start|>assistant\\n" + ("<think></think>" if think else "") + "<|audio_start|>' -}}"
|
||||
)
|
||||
|
||||
def set_vocab(self):
|
||||
super().set_vocab()
|
||||
# hparams hold the merged text config, the checkpoint's own model_type is in config.json
|
||||
with open(self.dir_model / "config.json", encoding="utf-8") as f:
|
||||
model_type = json.load(f).get("model_type")
|
||||
is_audio = model_type == "lfm3_audio"
|
||||
is_omni = model_type == "lfm3_omni"
|
||||
templates = []
|
||||
if is_audio:
|
||||
templates.append({"name": "asr", "template": self._asr_template})
|
||||
if is_audio or is_omni:
|
||||
# LFM3-Omni opens every assistant turn with an empty think block
|
||||
templates.append({"name": "tts", "template": self._tts_template(think=is_omni)})
|
||||
if templates:
|
||||
self.gguf_writer.add_chat_template(templates)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
layer_types = self.hparams["layer_types"]
|
||||
n_kv_heads = self.hparams["num_key_value_heads"]
|
||||
|
||||
# set num_key_value_heads only for attention layers
|
||||
self.hparams["num_key_value_heads"] = [
|
||||
n_kv_heads if lt != "conv" else 0
|
||||
for lt in layer_types
|
||||
]
|
||||
|
||||
super().set_gguf_parameters()
|
||||
|
||||
self.gguf_writer.add_rope_dimension_count(self.hparams["head_dim"])
|
||||
|
||||
self.gguf_writer.add_sliding_window_pattern([lt == "sliding_attention" for lt in layer_types])
|
||||
self.gguf_writer.add_sliding_window(self.hparams["sliding_window"])
|
||||
|
||||
self.gguf_writer.add_shortconv_l_cache(self.hparams["conv_L_cache"])
|
||||
|
||||
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
||||
# Partial RoPE is represented by masking trailing frequency pairs.
|
||||
# rope_parameters is keyed by layer type; only full_attention layers use partial RoPE.
|
||||
rope_parameters = self.hparams["rope_parameters"]["full_attention"]
|
||||
partial = rope_parameters.get("partial_rotary_factor", 1.0)
|
||||
head_dim = self.hparams["head_dim"]
|
||||
rope_angles = int(partial * head_dim) // 2
|
||||
n_pairs = head_dim // 2
|
||||
assert 0 < rope_angles <= n_pairs
|
||||
values = [1.0] * rope_angles + [1e30] * (n_pairs - rope_angles)
|
||||
rope_freqs = torch.tensor(values, dtype=torch.float32)
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FREQS), rope_freqs)
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
# conv op requires 2d tensor
|
||||
if "conv.conv" in name:
|
||||
data_torch = data_torch.squeeze(1)
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
|
||||
# d1 on LFM3-VL / LFM3-Omni: the stock weights, the d1 prompt read at its last token
|
||||
_D1_TYPES = {"lfm3_vl": gguf.DecisionType.LFM3_D1, "lfm3_omni": gguf.DecisionType.LFM3_D1_OMNI}
|
||||
|
||||
|
||||
def _is_lfm3_d1_checkpoint(dir_model: Path) -> bool:
|
||||
if not (dir_model / "config.json").is_file():
|
||||
return False
|
||||
with open(dir_model / "config.json", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
return config.get("architectures") == ["D1Model"] and config.get("model_type") in _D1_TYPES
|
||||
|
||||
|
||||
@ModelBase.register_hparams_loader(_is_lfm3_d1_checkpoint)
|
||||
def _load_lfm3_d1_hparams(dir_model: Path) -> dict[str, Any]:
|
||||
logger.info("gguf: detected LFM3 d1 checkpoint")
|
||||
hparams = ModelBase.load_hparams(dir_model, False, guess=False)
|
||||
hparams["d1_family"] = hparams["model_type"]
|
||||
# the mmproj is the base model's, the text model writes the decision
|
||||
hparams["architectures"] = ["Lfm3VlForConditionalGeneration" if hparams["model_type"] == "lfm3_vl" else "Lfm3OmniForConditionalGeneration"]
|
||||
hparams["text_config"]["architectures"] = ["Lfm3D1Model"]
|
||||
return hparams
|
||||
|
||||
|
||||
@ModelBase.register("Lfm3D1Model")
|
||||
@ModelBase.example("LiquidAI/d1-2B", "LiquidAI/d1-omni-2B")
|
||||
class LFM3D1Model(LFM3Model):
|
||||
model_arch = gguf.MODEL_ARCH.LFM3
|
||||
|
||||
# how the checkpoint serves, hard-coded in the server: assert the checkpoint records the same
|
||||
_served = {"d1_codes": 2, "d1_attention": "set"}
|
||||
|
||||
def set_vocab(self):
|
||||
# the vocab without the speech task templates of LFM3-Audio/Omni, d1 only decides
|
||||
TextModel.set_vocab(self)
|
||||
# the d1 prompt of LFM2, the LFM3 template writes an empty thought before the answer
|
||||
self.gguf_writer.add_chat_template([{"name": "systemone", "template": D1Model._systemone_template() + "<think></think>"}])
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
|
||||
for key, value in self._served.items():
|
||||
if self.hparams.get(key) != value:
|
||||
raise ValueError(f"unsupported d1 checkpoint: {key} = {self.hparams.get(key)!r}, expected {value!r}")
|
||||
|
||||
# a lone question reads the whole prompt both ways on the full attention layers
|
||||
self.gguf_writer.add_causal_attention(False)
|
||||
self.gguf_writer.add_decision_type(_D1_TYPES[self.hparams["d1_family"]])
|
||||
|
||||
# the option answers share one temperature
|
||||
with open(self.dir_model / "calibration.json", encoding="utf-8") as f:
|
||||
calibration = json.load(f)
|
||||
if calibration.get("temperatures"):
|
||||
raise ValueError("unsupported d1 calibration: per-bucket temperatures")
|
||||
for name in ("noul", "choice", "score"):
|
||||
self.gguf_writer.add_decision_temperature(name, calibration["default"])
|
||||
|
||||
|
||||
class LFM3VLVisionMixin(MmprojModel):
|
||||
"""LFM3-VL vision tower and projector, shared with LFM3-Omni."""
|
||||
|
||||
def _set_lfm3vl_gguf_parameters(self):
|
||||
assert self.hparams_vision is not None
|
||||
# clip.cpp hardcodes the GELU projector and its input norm eps
|
||||
if (self.global_config["projector_hidden_act"] != "gelu" or not self.global_config["projector_bias"]
|
||||
or self.global_config["projector_layer_norm_eps"] != 1e-6):
|
||||
raise ValueError("unsupported projector_hidden_act / projector_bias / projector_layer_norm_eps")
|
||||
# clip.cpp hardcodes the vision rope base, the RGB patchifier and the 3x3 spatial conv
|
||||
if (self.hparams_vision["rope_parameters"]["rope_theta"] != 1e6 or self.hparams_vision["in_channels"] != 3
|
||||
or self.hparams_vision["conv_kernel_size"] != 3):
|
||||
raise ValueError("unsupported vision rope_theta / in_channels / conv_kernel_size")
|
||||
self.gguf_writer.add_clip_vision_projector_type(gguf.VisionProjectorType.LFM3VL)
|
||||
self.gguf_writer.add_vision_head_count_kv(self.hparams_vision["num_key_value_heads"])
|
||||
self.gguf_writer.add_vision_spatial_merge_size(self.global_config["downsample_factor"])
|
||||
self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams_vision["norm_eps"])
|
||||
self.gguf_writer.add_vision_use_silu(True)
|
||||
size = self.preprocessor_config["size"]
|
||||
self.gguf_writer.add_vision_min_pixels(size["shortest_edge"])
|
||||
self.gguf_writer.add_vision_max_pixels(size["longest_edge"])
|
||||
# PIL resample enum, the torchvision backend has no lanczos
|
||||
resample = self.preprocessor_config["resample"]
|
||||
resize_algo = {2: "bilinear", 3: "bicubic"}.get(resample)
|
||||
if resize_algo is None:
|
||||
raise ValueError(f"unsupported resample: {resample}")
|
||||
self.gguf_writer.add_vision_image_resize_algo(resize_algo)
|
||||
self._check_video_config()
|
||||
|
||||
# mtmd_get_video_ts_style and the lfm3vl video frame sizing hardcode these, conversion fails on a mismatch
|
||||
_video = {"fps": 2.0, "min_frames": 4, "temporal_patch_size": 2, "size": {"shortest_edge": 4096, "longest_edge": 25165824}}
|
||||
_video_max_frames = 768
|
||||
|
||||
def _check_video_config(self):
|
||||
import json
|
||||
video = self.preprocessor_config.get("video_processor")
|
||||
path = self.dir_model / "video_preprocessor_config.json"
|
||||
if video is None and path.is_file():
|
||||
with open(path, encoding="utf-8") as f:
|
||||
video = json.load(f)
|
||||
if video is None:
|
||||
return
|
||||
for k, v in {**self._video, "max_frames": self._video_max_frames}.items():
|
||||
if video.get(k) != v:
|
||||
raise ValueError(f"unsupported video {k}={video.get(k)!r}, expected {v!r}")
|
||||
|
||||
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
||||
if "patch_embd" in new_name:
|
||||
return gguf.GGMLQuantizationType.F16
|
||||
if "conv2d_spatial" in new_name:
|
||||
# CUDA CONV_2D_DW requires an f32 kernel; the tensor is tiny
|
||||
return gguf.GGMLQuantizationType.F32
|
||||
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
||||
|
||||
def _lfm3vl_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name == "model.vision_tower.patchifier.weight":
|
||||
# qwen2vl-style layout [channel, temporal, ph, pw], one conv kernel per temporal slice
|
||||
assert self.hparams_vision is not None
|
||||
t = self.hparams_vision["temporal_patch_size"]
|
||||
p = self.hparams_vision["patch_size"]
|
||||
n_embd = data_torch.shape[0]
|
||||
w = data_torch.view(n_embd, 3, t, p, p)
|
||||
yield ("v.patch_embd.weight", w[:, :, 0].contiguous())
|
||||
if t == 2:
|
||||
yield ("v.patch_embd.weight.1", w[:, :, 1].contiguous())
|
||||
else:
|
||||
assert t == 1, f"unsupported vision_temporal_patch_size {t}"
|
||||
return
|
||||
if name == "model.vision_tower.embedding_norm.weight":
|
||||
yield ("v.embedding_norm.weight", data_torch)
|
||||
return
|
||||
if "conv.in_proj.weight" in name:
|
||||
yield (f"v.blk.{bid}.conv2d_in_proj.weight", data_torch)
|
||||
return
|
||||
if "conv.spatial_conv.weight" in name:
|
||||
yield (f"v.blk.{bid}.conv2d_spatial.weight", data_torch)
|
||||
return
|
||||
if "conv.out_proj.weight" in name:
|
||||
yield (f"v.blk.{bid}.conv2d_out_proj.weight", data_torch)
|
||||
return
|
||||
|
||||
# projector: input LayerNorm -> linear_1 -> GELU -> linear_2
|
||||
if name.startswith("model.multi_modal_projector."):
|
||||
layer, suffix = name.removeprefix("model.multi_modal_projector.").split(".")
|
||||
yield ({"layer_norm": "mm.input_norm", "linear_1": "mm.0", "linear_2": "mm.2"}[layer] + "." + suffix, data_torch)
|
||||
return
|
||||
|
||||
yield (self.map_tensor_name(name), data_torch)
|
||||
|
||||
|
||||
@ModelBase.register("Lfm3VlForConditionalGeneration")
|
||||
@ModelBase.example("LiquidAI/LFM3-VL-2B")
|
||||
class LFM3VLMmprojModel(LFM3VLVisionMixin):
|
||||
has_vision_encoder = True
|
||||
has_audio_encoder = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
assert self.hparams_vision is not None
|
||||
# dynamic resolution, image_size is only set for MmprojModel compatibility
|
||||
self.hparams_vision["image_size"] = 512
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self._set_lfm3vl_gguf_parameters()
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name.startswith(("model.vision_tower.", "model.multi_modal_projector.")):
|
||||
yield from self._lfm3vl_tensors(data_torch, name, bid)
|
||||
|
||||
|
||||
def _is_lfm3_audio_checkpoint(dir_model: Path) -> bool:
|
||||
if not (dir_model / "config.json").is_file():
|
||||
return False
|
||||
with open(dir_model / "config.json", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
return config.get("model_type") == "lfm3_audio" and "lfm" in config
|
||||
|
||||
|
||||
# read the release config.json as is: a transformers that knows lfm3_audio rewrites it into another layout
|
||||
@ModelBase.register_hparams_loader(_is_lfm3_audio_checkpoint)
|
||||
def _load_lfm3_audio_hparams(dir_model: Path) -> dict[str, Any]:
|
||||
with open(dir_model / "config.json", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
config["text_config"] = config["lfm"]
|
||||
return config
|
||||
|
||||
|
||||
@ModelBase.register("Lfm3AudioForConditionalGeneration")
|
||||
@ModelBase.example("LiquidAI/LFM3-Audio-2B")
|
||||
class LFM3AudioMmprojModel(MmprojModel):
|
||||
has_vision_encoder = False
|
||||
has_audio_encoder = True
|
||||
|
||||
# the downsampler has the LFM2-Audio pre-encoder layout: conv, dw conv, pw conv, dw conv, pw conv
|
||||
_downsample_idx = {"conv_in": 0, "conv_mid": 2, "lin_mid": 3, "conv_end": 5, "lin_end": 6}
|
||||
|
||||
# audio output: MaskGIT depthformer (a.gen.code.*) and the codec decoder (a.gen.wav.*)
|
||||
_depth_layer_map = {
|
||||
"operator_norm": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_NORM,
|
||||
"self_attn.q_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_Q,
|
||||
"self_attn.q_norm": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_Q_NORM,
|
||||
"self_attn.k_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_K,
|
||||
"self_attn.k_norm": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_K_NORM,
|
||||
"self_attn.v_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_V,
|
||||
"self_attn.o_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_OUT,
|
||||
"self_attn.gate_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_GATE,
|
||||
"ffn_norm": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_NORM,
|
||||
"feed_forward.w1": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_GATE,
|
||||
"feed_forward.w3": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_UP,
|
||||
"feed_forward.w2": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_DOWN,
|
||||
}
|
||||
_codec_layer_map = {
|
||||
"operator_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_NORM,
|
||||
"post_operator_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_POST_NORM,
|
||||
"self_attn.q_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_Q,
|
||||
"self_attn.q_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_Q_NORM,
|
||||
"self_attn.k_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_K,
|
||||
"self_attn.k_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_K_NORM,
|
||||
"self_attn.v_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_V,
|
||||
"self_attn.o_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_OUT,
|
||||
"self_attn.gate_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_GATE,
|
||||
"conv.in_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_CONV_PW1,
|
||||
"conv.conv": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_CONV_DW,
|
||||
"conv.out_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_CONV_PW2,
|
||||
"ffn_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_FFN_NORM,
|
||||
"post_ffn_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_FFN_POST_NORM,
|
||||
"feed_forward.w1": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_FFN_GATE,
|
||||
"feed_forward.w3": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_FFN_UP,
|
||||
"feed_forward.w2": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_FFN_DOWN,
|
||||
}
|
||||
# the codec graph in clip.cpp hardcodes the liquid-codec FSQ front end and decoder shape, conversion fails on a mismatch
|
||||
_codec = {
|
||||
"model_type": "liquid_codec", "fsq_levels": [8, 7, 6, 6], "n_q": 12, "quantizer_dim": 256,
|
||||
"encoder_dim": 512, "stft_n_fft": 1280, "stft_hop_length": 320, "stft_win_length": 1280, "stft_frame_stack": 6,
|
||||
"decoder_window": 30, "sampling_rate": 24000,
|
||||
}
|
||||
# the lfm3a pipeline in mtmd-helper-gen.cpp hardcodes these
|
||||
_generation = {"codebooks": 12, "maskgit_schedule": [4, 4, 4], "interleaved_n_text": 6, "interleaved_n_audio": 12}
|
||||
|
||||
# speech-code sampling of the model card, generation_config.json overrides it
|
||||
_audio_sampling = {
|
||||
"audio_temperature": 1.0, "audio_top_k": 128, "audio_repetition_penalty": 1.1, "audio_repetition_window": 4096,
|
||||
}
|
||||
|
||||
# mtmd_audio_preprocessor_lfm3a hardcodes this front end, conversion fails on a mismatch
|
||||
_feature_extractor = {
|
||||
"sampling_rate": 16000, "feature_size": 128, "n_fft": 400, "win_length": 400, "hop_length": 160,
|
||||
"log_floor": -12.0, "normalization_offset": 4.0, "normalization_divisor": 4.0, "encoder_stride": 8,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# audio output is exported when the checkpoint ships the codec
|
||||
self._audio_output = (self.dir_model / "audio_tokenizer").is_dir()
|
||||
gen = self.get_generation_config()
|
||||
self._n_codebooks: int = gen["codebooks"]
|
||||
self._audio_vocab: int = gen["audio_vocab_size"]
|
||||
self._depth_embd: dict[int, Tensor] = {}
|
||||
self._depth_norm: dict[int, Tensor] = {}
|
||||
|
||||
def get_audio_config(self) -> dict[str, Any] | None:
|
||||
enc = self.global_config["encoder"]
|
||||
cfg = dict(enc["lfm"])
|
||||
cfg["num_mel_bins"] = enc["dim_in"]
|
||||
cfg["sliding_window"] = enc["sliding_window"]
|
||||
return cfg
|
||||
|
||||
def get_generation_config(self) -> dict[str, Any]:
|
||||
c = self.global_config
|
||||
steps = c["maskgit_steps"]
|
||||
return {
|
||||
"codebooks": c["codebooks"], "audio_vocab_size": c["audio_vocab_size"],
|
||||
"maskgit_schedule": [c["codebooks"] // steps] * steps,
|
||||
"interleaved_n_text": c["interleaved_n_text"], "interleaved_n_audio": c["interleaved_n_audio"],
|
||||
}
|
||||
|
||||
def get_depth_config(self) -> dict[str, Any]:
|
||||
return self.global_config["depthformer"]
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
assert self.hparams_audio is not None
|
||||
fe = self.preprocessor_config["feature_extractor"]
|
||||
if fe["feature_extractor_type"] not in ("Lfm3AudioFeatureExtractor", "Lfm3OmniFeatureExtractor"):
|
||||
raise ValueError(f"unsupported feature_extractor_type {fe['feature_extractor_type']!r}")
|
||||
for k, v in self._feature_extractor.items():
|
||||
if fe[k] != v:
|
||||
raise ValueError(f"unsupported feature_extractor.{k}={fe[k]!r}, expected {v!r}")
|
||||
|
||||
# clip.cpp hardcodes the encoder rope base
|
||||
if self.hparams_audio["rope_parameters"]["rope_theta"] != 1e6:
|
||||
raise ValueError("unsupported audio encoder rope_theta")
|
||||
|
||||
self.hparams_audio["intermediate_size"] = lfm2_feed_forward_length(self.hparams_audio)
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_clip_audio_projector_type(gguf.VisionProjectorType.LFM3A)
|
||||
self.gguf_writer.add_audio_head_count_kv(self.hparams_audio["num_key_value_heads"])
|
||||
self.gguf_writer.add_audio_attention_layernorm_eps(self.hparams_audio["norm_eps"])
|
||||
self.gguf_writer.add_audio_num_mel_bins(self.hparams_audio["num_mel_bins"])
|
||||
self.gguf_writer.add_audio_window_size(self.hparams_audio["sliding_window"])
|
||||
|
||||
if not self._audio_output:
|
||||
return
|
||||
|
||||
# audio generation: depthformer hparams, the codec ones are constant and live in clip.cpp
|
||||
gen = self.get_generation_config()
|
||||
for k, v in self._generation.items():
|
||||
if gen[k] != v:
|
||||
raise ValueError(f"unsupported {k}={gen[k]!r}, expected {v!r}")
|
||||
depth = self.get_depth_config()
|
||||
if depth.get("is_causal", False) or depth["use_sandwich_norm"] or set(depth["layer_types"]) != {"full_attention"}:
|
||||
raise ValueError("unsupported depthformer, expected a bidirectional all-attention stack")
|
||||
if depth["rope_parameters"]["full_attention"]["rope_theta"] != 1e6:
|
||||
raise ValueError("unsupported depthformer rope_theta")
|
||||
self.gguf_writer.add_clip_has_gen_audio_encoder(True)
|
||||
self.gguf_writer.add_clip_gen_audio_projector_type(gguf.VisionProjectorType.LFM3A_GEN)
|
||||
self.gguf_writer.add_gen_audio_projection_dim(self.n_embd_text)
|
||||
self.gguf_writer.add_gen_audio_embedding_length(depth["hidden_size"])
|
||||
self.gguf_writer.add_gen_audio_feed_forward_length(depth["intermediate_size"])
|
||||
self.gguf_writer.add_gen_audio_block_count(depth["num_hidden_layers"])
|
||||
self.gguf_writer.add_gen_audio_head_count(depth["num_attention_heads"])
|
||||
self.gguf_writer.add_gen_audio_head_count_kv(depth["num_key_value_heads"])
|
||||
self.gguf_writer.add_gen_audio_attention_layernorm_eps(depth["norm_eps"])
|
||||
|
||||
# sampling defaults of the speech codes, next to the text ones that become general.sampling.*
|
||||
gen = gguf.Metadata.load_generation_config(self.dir_model)
|
||||
for key, add in (("audio_temperature", self.gguf_writer.add_gen_audio_sampling_temp),
|
||||
("audio_top_k", self.gguf_writer.add_gen_audio_sampling_top_k),
|
||||
("audio_repetition_penalty", self.gguf_writer.add_gen_audio_sampling_penalty_repeat),
|
||||
("audio_repetition_window", self.gguf_writer.add_gen_audio_sampling_penalty_last_n)):
|
||||
value = gen.get(key, self._audio_sampling.get(key))
|
||||
if value is not None:
|
||||
add(value)
|
||||
|
||||
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
||||
if new_name.startswith("a.conv1d.") or "conv_dw" in new_name or new_name.startswith("a.gen.wav.quant."):
|
||||
return gguf.GGMLQuantizationType.F32
|
||||
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
||||
|
||||
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
||||
if self._audio_output:
|
||||
yield from self._codec_tensors()
|
||||
|
||||
def _codec_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
||||
import json
|
||||
from safetensors.torch import load_file
|
||||
|
||||
T = gguf.MODEL_TENSOR
|
||||
codec_dir = self.dir_model / "audio_tokenizer"
|
||||
with open(codec_dir / "config.json", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
for k, v in self._codec.items():
|
||||
if cfg.get(k) != v:
|
||||
raise ValueError(f"unsupported audio_tokenizer {k}={cfg.get(k)!r}, expected {v!r}")
|
||||
if math.prod(cfg["fsq_levels"]) != self._audio_vocab - 1:
|
||||
raise ValueError(f"audio_vocab_size {self._audio_vocab} does not match the codec, expected codebook size + 1")
|
||||
|
||||
state_dict = load_file(codec_dir / "model.safetensors")
|
||||
|
||||
# FSQ code table: index -> the len(levels) quantized values, see FiniteScalarQuantization._indices_to_codes
|
||||
levels = torch.tensor(cfg["fsq_levels"], dtype=torch.int64)
|
||||
basis = torch.cumprod(torch.cat([torch.ones(1, dtype=torch.int64), levels[:-1]]), dim=0)
|
||||
half = levels // 2
|
||||
idx = torch.arange(int(levels.prod()), dtype=torch.int64)[:, None]
|
||||
codes = ((idx // basis) % levels - half) / half
|
||||
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_FIRST_CB), codes.to(torch.float32))
|
||||
|
||||
# one projection over the concatenated code vectors of all codebooks, under quantizer.fsq. in newer exports
|
||||
quantizer = "quantizer.fsq." if "quantizer.fsq.project_out.weight" in state_dict else "quantizer."
|
||||
for suffix in ("weight", "bias"):
|
||||
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_FIRST_OUT, suffix=f".{suffix}"), state_dict[f"{quantizer}project_out.{suffix}"])
|
||||
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_IN_PROJ, suffix=f".{suffix}"), state_dict[f"from_quantizer.{suffix}"])
|
||||
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_OUT_PROJ, suffix=f".{suffix}"), state_dict[f"decoder.lin.{suffix}"])
|
||||
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_OUTPUT_NORM), state_dict["decoder.lfm.embedding_norm.weight"])
|
||||
|
||||
for name, data_torch in state_dict.items():
|
||||
if not name.startswith("decoder.lfm.layers."):
|
||||
continue
|
||||
bid_str, key = name.removeprefix("decoder.lfm.layers.").rsplit(".weight", 1)[0].split(".", 1)
|
||||
if key == "conv.conv":
|
||||
data_torch = data_torch.squeeze(1)
|
||||
yield (self.format_tensor_name(self._codec_layer_map[key], int(bid_str)), data_torch)
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name.startswith("a.gen.wav."):
|
||||
yield (name, data_torch)
|
||||
return
|
||||
|
||||
if name.startswith(("depthformer.", "depth_", "codebook_embedding.", "mask_embedding")):
|
||||
if self._audio_output:
|
||||
yield from self._depthformer_tensor(data_torch, name, bid)
|
||||
return
|
||||
|
||||
if not name.startswith(("encoder.", "mlp.")):
|
||||
return
|
||||
|
||||
if name.startswith("encoder.downsample."):
|
||||
layer, suffix = name.removeprefix("encoder.downsample.").split(".")
|
||||
if layer == "lin_out":
|
||||
if suffix == "weight":
|
||||
data_torch = data_torch.squeeze(-1)
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_ENC_OUT, suffix=f".{suffix}"), data_torch)
|
||||
return
|
||||
if suffix == "bias":
|
||||
data_torch = data_torch[:, None, None]
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_ENC_CONV1D, self._downsample_idx[layer], f".{suffix}"), data_torch)
|
||||
return
|
||||
|
||||
# conv op requires 2d tensor
|
||||
if name.endswith("conv.conv.weight"):
|
||||
data_torch = data_torch.squeeze(1)
|
||||
|
||||
yield (self.map_tensor_name(name), data_torch)
|
||||
|
||||
def _depthformer_tensor(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
T = gguf.MODEL_TENSOR
|
||||
|
||||
if name.startswith("depthformer.model.layers."):
|
||||
key = name.removeprefix("depthformer.model.layers.").split(".", 1)[1].rsplit(".weight", 1)[0]
|
||||
yield (self.format_tensor_name(self._depth_layer_map[key], bid), data_torch)
|
||||
return
|
||||
if name == "depthformer.model.embedding_norm.weight":
|
||||
yield (self.format_tensor_name(T.A_GEN_CODE_OUTPUT_NORM), data_torch)
|
||||
return
|
||||
if name.startswith("depth_linear."):
|
||||
yield (self.format_tensor_name(T.A_GEN_CODE_PROJ_IN, suffix="." + name.rsplit(".", 1)[1]), data_torch)
|
||||
return
|
||||
if name == "mask_embedding":
|
||||
yield (self.format_tensor_name(T.A_GEN_CODE_MASK_EMBD, suffix=""), data_torch)
|
||||
return
|
||||
if name == "codebook_embedding.weight":
|
||||
assert data_torch.shape[0] == self._n_codebooks * self._audio_vocab
|
||||
yield (self.format_tensor_name(T.A_GEN_CODE_OUT_EMBD), data_torch)
|
||||
return
|
||||
|
||||
# per-codebook heads: the embedding table is tied to to_logits, merge the 12 tables into one tensor
|
||||
assert name.startswith("depth_embeddings.")
|
||||
idx = int(name.split(".")[1])
|
||||
key = name.split(".", 2)[2]
|
||||
if key == "embedding.weight":
|
||||
self._depth_embd[idx] = data_torch
|
||||
if len(self._depth_embd) == self._n_codebooks:
|
||||
yield (self.format_tensor_name(T.A_GEN_CODE_EMBD), torch.stack([self._depth_embd[i] for i in range(self._n_codebooks)], dim=0))
|
||||
elif key == "embedding_norm.weight":
|
||||
self._depth_norm[idx] = data_torch
|
||||
if len(self._depth_norm) == self._n_codebooks:
|
||||
yield (self.format_tensor_name(T.A_GEN_CODE_HEAD_NORM), torch.stack([self._depth_norm[i] for i in range(self._n_codebooks)], dim=0))
|
||||
elif key == "to_logits.weight":
|
||||
if not torch.equal(data_torch, self._depth_embd[idx]):
|
||||
raise ValueError(f"depth_embeddings.{idx} is not tied")
|
||||
else:
|
||||
raise ValueError(f"unexpected tensor {name}")
|
||||
|
||||
|
||||
@ModelBase.register("Lfm3OmniForConditionalGeneration")
|
||||
@ModelBase.example("LiquidAI/LFM3-Omni-2B")
|
||||
class LFM3OmniMmprojModel(LFM3VLVisionMixin, LFM3AudioMmprojModel):
|
||||
has_vision_encoder = True
|
||||
has_audio_encoder = True
|
||||
|
||||
_audio_sampling = {"audio_temperature": 0.4, "audio_repetition_penalty": 1.2, "audio_repetition_window": 64}
|
||||
_video_max_frames = 384
|
||||
|
||||
# the audio side is the LFM3-Audio model under the HF-native names
|
||||
_audio_renames = (
|
||||
("model.audio_encoder.lfm.", "encoder.lfm.lfm."),
|
||||
("model.audio_encoder.", "encoder."),
|
||||
("model.audio_projector.layer_norm.", "mlp.model.0."),
|
||||
("model.audio_projector.linear_1.", "mlp.model.1."),
|
||||
("model.audio_projector.linear_2.", "mlp.model.3."),
|
||||
("model.depth_decoder.model.", "depthformer.model."),
|
||||
("model.depth_decoder.", ""),
|
||||
("model.codebook_embedding.", "codebook_embedding."),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
assert self.hparams_vision is not None
|
||||
# dynamic resolution, image_size is only set for MmprojModel compatibility
|
||||
self.hparams_vision["image_size"] = 512
|
||||
|
||||
def get_audio_config(self) -> dict[str, Any] | None:
|
||||
enc = self.global_config["audio_encoder_config"]
|
||||
cfg = dict(enc["text_config"])
|
||||
cfg["num_mel_bins"] = enc["num_mel_bins"]
|
||||
cfg["sliding_window"] = enc["sliding_window"]
|
||||
return cfg
|
||||
|
||||
def get_generation_config(self) -> dict[str, Any]:
|
||||
c = self.global_config
|
||||
return {
|
||||
"codebooks": c["num_codebooks"], "audio_vocab_size": c["codebook_size"], "maskgit_schedule": c["maskgit_schedule"],
|
||||
"interleaved_n_text": c["interleaved_n_text"], "interleaved_n_audio": c["interleaved_n_audio"],
|
||||
}
|
||||
|
||||
def get_depth_config(self) -> dict[str, Any]:
|
||||
return self.global_config["depth_config"]
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self._set_lfm3vl_gguf_parameters()
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name.startswith(("model.vision_tower.", "model.multi_modal_projector.")):
|
||||
yield from self._lfm3vl_tensors(data_torch, name, bid)
|
||||
return
|
||||
for old, new in self._audio_renames:
|
||||
if name.startswith(old):
|
||||
yield from super().modify_tensors(data_torch, new + name.removeprefix(old), bid)
|
||||
return
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
@@ -0,0 +1,184 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import ModelBase, TextModel, gguf
|
||||
|
||||
|
||||
@ModelBase.register("LimiteForCausalLM")
|
||||
@ModelBase.example("paradigma-inc/limite-1b-violetto")
|
||||
class LimiteModel(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.LIMITE
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
hp = self.hparams
|
||||
|
||||
expected = {
|
||||
"pos_mode": "rope",
|
||||
"qk_norm": "rms_pre_rope",
|
||||
"rope_style": "interleaved_pairs_odd_lane_sign_flip",
|
||||
"rope_per_layer": False,
|
||||
"rms_norm_has_weight": False,
|
||||
"rms_norm_eps_mode": "torch_finfo_default",
|
||||
"mlp_type": "swiglu",
|
||||
"attn_gate_applied": "per_head_before_o_proj",
|
||||
"ve_head_slice": "first_num_key_value_heads",
|
||||
"ve_applied_before_qk_norm": True,
|
||||
"global_nope": True,
|
||||
"global_window": -1,
|
||||
"mudd_r_site": "resid",
|
||||
"tie_word_embeddings": True,
|
||||
"final_softcap": 0.0,
|
||||
}
|
||||
for key, value in expected.items():
|
||||
if hp.get(key) != value:
|
||||
raise NotImplementedError(f"Limite: unsupported {key}={hp.get(key)!r}, expected {value!r}")
|
||||
if hp["softcap_logits"]["kind"] != "sigmoid":
|
||||
raise NotImplementedError(f"Limite: unsupported softcap_logits kind {hp['softcap_logits']['kind']!r}")
|
||||
|
||||
self.n_head = hp["num_attention_heads"]
|
||||
self.n_head_kv = hp["num_key_value_heads"]
|
||||
self.head_dim = hp["head_dim"]
|
||||
self.n_pairs = hp["rope_n_pairs"]
|
||||
|
||||
self.mudd_taps: dict[int, list[int]] = {}
|
||||
if hp.get("mudd"):
|
||||
if not hp.get("mudd_mlp"):
|
||||
raise NotImplementedError("Limite: mudd without mudd_mlp is not supported")
|
||||
self.mudd_taps = {int(il): [int(t) for t in taps] for il, taps in hp["mudd_tap_idx"].items()}
|
||||
assert all(len(taps) == hp["mudd_taps"] for taps in self.mudd_taps.values())
|
||||
|
||||
def set_vocab(self):
|
||||
self._set_vocab_gpt2()
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
hp = self.hparams
|
||||
|
||||
self.gguf_writer.add_key_length(self.head_dim)
|
||||
self.gguf_writer.add_value_length(self.head_dim)
|
||||
|
||||
# torch rms_norm with eps=None uses the fp32 epsilon, even for bf16 inputs
|
||||
self.gguf_writer.add_layer_norm_rms_eps(torch.finfo(torch.float32).eps)
|
||||
self.gguf_writer.add_attention_scale(hp["attention_softmax_scale"])
|
||||
self.gguf_writer.add_xsa_eps(hp["xsa_normalize_eps"] if hp.get("xsa") else 0.0)
|
||||
|
||||
# HF freq_i = base^(-i/(n_pairs-1)), ggml uses freq_base^(-i/n_pairs)
|
||||
self.gguf_writer.add_rope_dimension_count(2 * self.n_pairs)
|
||||
self.gguf_writer.add_rope_freq_base(hp["rope_base_local"] ** (self.n_pairs / (self.n_pairs - 1)))
|
||||
|
||||
# HF keeps keys k >= q - window, so a window of W spans W + 1 tokens
|
||||
global_layers = set(hp["global_layers"])
|
||||
self.gguf_writer.add_sliding_window(hp["sliding_window"] + 1)
|
||||
self.gguf_writer.add_sliding_window_pattern([il not in global_layers for il in range(self.block_count)])
|
||||
|
||||
cap = hp["softcap_logits"]
|
||||
self.gguf_writer.add_final_logit_sigmoid_capping(cap["a"], cap["b"], cap["c"])
|
||||
|
||||
if self.mudd_taps:
|
||||
n_taps = hp["mudd_taps"]
|
||||
tap_indices = [-1] * (self.block_count * n_taps)
|
||||
for il, taps in self.mudd_taps.items():
|
||||
tap_indices[il * n_taps:(il + 1) * n_taps] = taps
|
||||
self.gguf_writer.add_mudd_feed_forward_length(hp["mudd_inter"])
|
||||
self.gguf_writer.add_mudd_tap_count(n_taps)
|
||||
self.gguf_writer.add_mudd_tap_indices(tap_indices)
|
||||
|
||||
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
||||
# tiny mixing weights, kept at full precision as in the reference
|
||||
# the mudd tensors come from a global source tensor, so take the block id from the new name
|
||||
if new_name.startswith("blk."):
|
||||
bid = int(new_name.split(".")[1])
|
||||
for key in (gguf.MODEL_TENSOR.ATTN_GATE, gguf.MODEL_TENSOR.ATTN_VE_GATE, gguf.MODEL_TENSOR.MUDD_DOWN, gguf.MODEL_TENSOR.MUDD_UP):
|
||||
if self.match_model_tensor_name(new_name, key, bid):
|
||||
return gguf.GGMLQuantizationType.F32
|
||||
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
||||
|
||||
def _swap_rope_pairs(self, w: Tensor, n_head: int) -> Tensor:
|
||||
# HF rotates each pair by -theta; swapping the pair lanes gives the ggml +theta rotation.
|
||||
# q and k are both swapped, so the attention scores do not change.
|
||||
idx = torch.arange(self.head_dim)
|
||||
idx[:2 * self.n_pairs] = idx[:2 * self.n_pairs].view(-1, 2).flip(-1).reshape(-1)
|
||||
w = w.reshape(n_head, self.head_dim, -1)
|
||||
return w[:, idx, :].reshape(n_head * self.head_dim, -1)
|
||||
|
||||
def _pad_cols(self, w: Tensor) -> Tensor:
|
||||
# gates read only the first channels of the input, pad them to n_embd
|
||||
return torch.nn.functional.pad(w, (0, self.hparams["hidden_size"] - w.shape[-1]))
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
hp = self.hparams
|
||||
T = gguf.MODEL_TENSOR
|
||||
|
||||
def load(n: str) -> Tensor:
|
||||
return self.model_tensors[n]().float()
|
||||
|
||||
if name == "model.value_embeds.weight":
|
||||
ve = data_torch.reshape(data_torch.shape[0], -1, hp["ve_dim"])[:, :self.n_head_kv]
|
||||
ve = torch.nn.functional.pad(ve, (0, self.head_dim - hp["ve_dim"]))
|
||||
yield self.format_tensor_name(T.VALUE_EMBD), ve.reshape(ve.shape[0], -1) * hp["ve_gate_scale"]
|
||||
return
|
||||
|
||||
if name == "model.mudd.dense1":
|
||||
for il in self.mudd_taps:
|
||||
yield self.format_tensor_name(T.MUDD_DOWN, il), data_torch
|
||||
return
|
||||
if name == "model.mudd.dense2":
|
||||
# rows [0, n) mix the attention input, rows [n, 2n) mix the residual
|
||||
dense2_mlp = load("model.mudd.dense2_mlp")
|
||||
bias = load("model.mudd.bias")
|
||||
bias_mlp = load("model.mudd.bias_mlp")
|
||||
for il, taps in self.mudd_taps.items():
|
||||
n = len(taps)
|
||||
w = torch.cat([data_torch[il, :n], dense2_mlp[il, :n]], 0)
|
||||
b = torch.cat([bias[il, :n], bias_mlp[il, :n]], 0)
|
||||
yield self.format_tensor_name(T.MUDD_UP, il), w
|
||||
yield self.format_tensor_name(T.MUDD_UP, il, ".bias"), b
|
||||
return
|
||||
if name.startswith("model.mudd."):
|
||||
return
|
||||
|
||||
if bid is None:
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
return
|
||||
|
||||
prefix = f"model.layers.{bid}."
|
||||
name = name.removeprefix(prefix)
|
||||
|
||||
if name in ("self_attn.q_proj.weight", "self_attn.k_proj.weight", "self_attn.v_proj.weight"):
|
||||
data_torch = data_torch * load(prefix + "self_attn.qkv_scale")
|
||||
if name == "self_attn.q_proj.weight":
|
||||
data_torch = self._swap_rope_pairs(data_torch, self.n_head)
|
||||
elif name == "self_attn.k_proj.weight":
|
||||
data_torch = self._swap_rope_pairs(data_torch, self.n_head_kv)
|
||||
elif name == "self_attn.o_proj.weight":
|
||||
data_torch = data_torch * load(prefix + "self_attn.o_scale") * load(prefix + "post_lambda_attn") * hp["attn_gate_scale"]
|
||||
elif name == "mlp.down_proj.weight":
|
||||
data_torch = data_torch * load(prefix + "post_lambda_mlp")
|
||||
elif name == "self_attn.attn_gate":
|
||||
yield self.format_tensor_name(T.ATTN_GATE, bid), self._pad_cols(data_torch)
|
||||
return
|
||||
elif name == "self_attn.ve_gate":
|
||||
yield self.format_tensor_name(T.ATTN_VE_GATE, bid), self._pad_cols(data_torch[:self.n_head_kv])
|
||||
return
|
||||
elif name == "self_attn.xsa_alpha":
|
||||
if hp.get("xsa") and bid in hp["xsa_layers"]:
|
||||
yield self.format_tensor_name(T.ATTN_XSA_ALPHA, bid), torch.tanh(data_torch)
|
||||
return
|
||||
elif name == "resid_lambda_attn":
|
||||
yield self.format_tensor_name(T.ATTN_RESID_SCALE, bid), data_torch.reshape(1)
|
||||
return
|
||||
elif name == "resid_lambda_mlp":
|
||||
yield self.format_tensor_name(T.FFN_RESID_SCALE, bid), data_torch.reshape(1)
|
||||
return
|
||||
elif name in ("self_attn.qkv_scale", "self_attn.o_scale", "post_lambda_attn", "post_lambda_mlp"):
|
||||
# folded into the projections above
|
||||
return
|
||||
|
||||
yield from super().modify_tensors(data_torch, prefix + name, bid)
|
||||
+3
-2
@@ -129,8 +129,9 @@ class LlamaModel(TextModel):
|
||||
tokenizer_config_json = json.load(f)
|
||||
if (add_prefix_space := tokenizer_config_json.get("add_prefix_space")) is not None:
|
||||
self.gguf_writer.add_add_space_prefix(add_prefix_space)
|
||||
if tokenizer_config_json.get("tokenizer_class") == "HybridDNATokenizer":
|
||||
return self._set_vocab_hybriddna()
|
||||
|
||||
if self._is_hybriddna():
|
||||
return self._set_vocab_hybriddna()
|
||||
|
||||
try:
|
||||
self._set_vocab_sentencepiece()
|
||||
|
||||
+22
-1
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from typing import Iterable, TYPE_CHECKING
|
||||
from typing import Callable, Iterable, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
@@ -128,3 +128,24 @@ class LlavaVisionModel(MmprojModel):
|
||||
yield from super().modify_tensors(img_break_embd, name, bid)
|
||||
|
||||
return # skip other tensors
|
||||
|
||||
|
||||
@ModelBase.register("Mistral3Model")
|
||||
@ModelBase.example("nvidia/Nemotron-3.5-Embed-1B-BF16")
|
||||
class Mistral3EmbeddingVisionModel(LlavaVisionModel):
|
||||
def __init__(self, dir_model, *args, **kwargs):
|
||||
hparams = ModelBase.load_hparams(dir_model, is_mistral_format=False)
|
||||
text_config = hparams.get("text_config", hparams)
|
||||
if text_config.get("is_causal") is not False:
|
||||
raise NotImplementedError("Expected a Nemotron Embed model for Mistral3Model, but is_causal is not False")
|
||||
super().__init__(dir_model, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
if item[0].endswith("embed_tokens.weight"):
|
||||
return ModelBase.filter_tensors(item)
|
||||
return super().filter_tensors(item)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_vision_preproc_image_size(self.preprocessor_config["size"]["longest_edge"])
|
||||
|
||||
+73
-4
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, TYPE_CHECKING
|
||||
from typing import Callable, Iterable, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
@@ -14,6 +14,54 @@ from .base import ModelBase, TextModel, gguf, logger
|
||||
@ModelBase.example("JetBrains/Mellum2-12B-A2.5B-Base")
|
||||
class MellumModel(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.MELLUM
|
||||
supports_mtp_export = True
|
||||
|
||||
_n_main_layers: int | None = None
|
||||
|
||||
_experts: list[dict[str, Tensor]] | None = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# number of exported MTP blocks, appended after the trunk layers
|
||||
self.n_nextn = 0 if self.no_mtp else (self.find_hparam(["num_nextn_predict_layers", "num_mtp_layers"], optional=True) or 0)
|
||||
if self.mtp_only and self.n_nextn == 0:
|
||||
raise ValueError("--mtp requested, but the checkpoint has no MTP layers (num_nextn_predict_layers is 0)")
|
||||
if self.n_nextn > 0:
|
||||
if any(t != "sparse" for t in self.hparams.get("mtp_mlp_layer_types") or []):
|
||||
raise ValueError("Mellum MTP layers with dense MLP are not supported")
|
||||
self.block_count += self.n_nextn
|
||||
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
||||
|
||||
def index_tensors(self, remote_hf_model_id: str | None = None):
|
||||
type(self)._n_main_layers = self.hparams["num_hidden_layers"]
|
||||
return super().index_tensors(remote_hf_model_id=remote_hf_model_id)
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
if (titem := super().filter_tensors(item)) is None:
|
||||
return None
|
||||
name, gen = titem
|
||||
|
||||
if name.startswith("mtp."):
|
||||
if cls.no_mtp:
|
||||
return None
|
||||
assert cls._n_main_layers is not None
|
||||
# mtp.layers.{i}.* -> model.layers.{n_main + i}.*, mtp.{fc,norm,...} -> nextn names of the first MTP block
|
||||
remapper = {
|
||||
"fc": "eh_proj",
|
||||
"pre_fc_norm_embedding": "enorm",
|
||||
"pre_fc_norm_hidden": "hnorm",
|
||||
"norm": "shared_head.norm",
|
||||
}
|
||||
parts = name.split(".", 3)
|
||||
if len(parts) == 4 and parts[1] == "layers" and parts[2].isdecimal():
|
||||
name = f"model.layers.{cls._n_main_layers + int(parts[2])}.{parts[3]}"
|
||||
elif len(parts) == 3 and parts[1] in remapper:
|
||||
name = f"model.layers.{cls._n_main_layers}.{remapper[parts[1]]}.{parts[2]}"
|
||||
elif cls.mtp_only and name not in ("model.embed_tokens.weight", "model.norm.weight", "lm_head.weight"):
|
||||
return None
|
||||
|
||||
return name, gen
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
@@ -26,10 +74,31 @@ class MellumModel(TextModel):
|
||||
if (use_sliding_window is True or use_sliding_window is None) and sliding_window is not None:
|
||||
self.gguf_writer.add_sliding_window(sliding_window)
|
||||
logger.info(f"gguf: sliding window = {sliding_window}")
|
||||
self.gguf_writer.add_sliding_window_pattern([t == "sliding_attention" for t in self.hparams["layer_types"]])
|
||||
logger.info(f"gguf: sliding window pattern length = {len(self.hparams['layer_types'])}")
|
||||
layer_types = list(self.hparams["layer_types"])
|
||||
# MTP blocks default to sliding window attention, as in transformers
|
||||
if self.n_nextn > 0:
|
||||
layer_types += self.hparams.get("mtp_layer_types") or ["sliding_attention"] * self.n_nextn
|
||||
self.gguf_writer.add_sliding_window_pattern([t == "sliding_attention" for t in layer_types])
|
||||
logger.info(f"gguf: sliding window pattern length = {len(layer_types)}")
|
||||
|
||||
_experts: list[dict[str, Tensor]] | None = None
|
||||
if (norm_topk_prob := self.hparams.get("norm_topk_prob")) is not None:
|
||||
self.gguf_writer.add_expert_weights_norm(norm_topk_prob)
|
||||
|
||||
if self.n_nextn > 0:
|
||||
self.gguf_writer.add_nextn_predict_layers(self.n_nextn)
|
||||
|
||||
def prepare_metadata(self, vocab_only: bool):
|
||||
from_dir = self.fname_out.is_dir()
|
||||
super().prepare_metadata(vocab_only=vocab_only)
|
||||
|
||||
if not self.mtp_only or not from_dir:
|
||||
return
|
||||
|
||||
output_type: str = self.ftype.name.partition("_")[2]
|
||||
fname_default: str = gguf.naming_convention(
|
||||
self.metadata.name, self.metadata.basename, self.metadata.finetune,
|
||||
self.metadata.version, size_label=None, output_type=output_type, model_type=None)
|
||||
self.fname_out = self.fname_out.parent / f"mtp-{fname_default}.gguf"
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name.find("experts") != -1:
|
||||
|
||||
@@ -272,3 +272,18 @@ class MiniCPMV4_7VisionModel(MiniCPMV4_6VisionModel):
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.V_TOK_EMBD_SEP, suffix=""), data_torch[ids]
|
||||
return
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
|
||||
# MiniCPM-OCR 1.0: the 4.6 stack with model_type minicpm_ocr.
|
||||
|
||||
@ModelBase.register("MiniCPMOcrForConditionalGeneration")
|
||||
@ModelBase.example("openbmb/MiniCPM-OCR-1.0")
|
||||
class MiniCPMOCR1_0TextModel(MiniCPMV4_6TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.QWEN35
|
||||
no_mtp = True
|
||||
|
||||
|
||||
@ModelBase.register("MiniCPMOcrForConditionalGeneration")
|
||||
@ModelBase.example("openbmb/MiniCPM-OCR-1.0")
|
||||
class MiniCPMOCR1_0VisionModel(MiniCPMV4_6VisionModel):
|
||||
projector_type = gguf.VisionProjectorType.MINICPM_OCR_1_0
|
||||
|
||||
+43
-6
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import ModelBase, TextModel, gguf
|
||||
from .base import ModelBase, TextModel, get_model_architecture, gguf
|
||||
|
||||
from .deepseek import DeepseekV2Model
|
||||
from .llama import LlamaModel
|
||||
@@ -13,9 +13,11 @@ from .llama import LlamaModel
|
||||
|
||||
@ModelBase.register(
|
||||
"Mistral3ForConditionalGeneration",
|
||||
"Mistral3Model",
|
||||
"Ministral3ForCausalLM",
|
||||
"Ministral3Model",
|
||||
)
|
||||
@ModelBase.example("mistralai/Mistral-Small-3.1-24B-Instruct-2503", "hf-tiny-v2/tiny-random-Ministral3ForCausalLM")
|
||||
@ModelBase.example("mistralai/Mistral-Small-3.1-24B-Instruct-2503", "hf-tiny-v2/tiny-random-Ministral3ForCausalLM", "nvidia/Nemotron-3-Embed-1B-BF16", "nvidia/Nemotron-3.5-Embed-1B-BF16")
|
||||
class Mistral3Model(TextModel):
|
||||
class Ministral3Model(LlamaModel):
|
||||
model_arch = gguf.MODEL_ARCH.MISTRAL3
|
||||
@@ -29,6 +31,23 @@ class Mistral3Model(TextModel):
|
||||
self.gguf_writer.add_rope_scaling_yarn_log_mul(rope_params["mscale_all_dim"])
|
||||
self.gguf_writer.add_attn_temperature_scale(rope_params["llama_4_scaling_beta"])
|
||||
|
||||
class Mistral3EmbedNemotronModel(Ministral3Model):
|
||||
model_arch = gguf.MODEL_ARCH.MISTRAL3
|
||||
|
||||
def set_vocab(self):
|
||||
super().set_vocab()
|
||||
self.gguf_writer.add_add_bos_token(False)
|
||||
self.gguf_writer.add_add_eos_token(False)
|
||||
|
||||
def get_vocab_base_pre(self, tokenizer) -> str:
|
||||
if self.origin_hf_arch == "Mistral3Model":
|
||||
return "nemotron-3.5-embed"
|
||||
return super().get_vocab_base_pre(tokenizer)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self._try_set_pooling_type()
|
||||
|
||||
class Mistral4Model(DeepseekV2Model):
|
||||
model_arch = gguf.MODEL_ARCH.MISTRAL4
|
||||
skip_mtp = False # model contains no MTP layers, so no need to skip
|
||||
@@ -42,12 +61,20 @@ class Mistral3Model(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.MISTRAL3 # unused
|
||||
impl: TextModel
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, dir_model, *args, **kwargs):
|
||||
hparams = ModelBase.load_hparams(dir_model, is_mistral_format=False)
|
||||
hf_arch = get_model_architecture(hparams, self.model_type)
|
||||
if hf_arch in ("Ministral3Model", "Mistral3Model"):
|
||||
text_config = hparams.get("text_config", hparams)
|
||||
if text_config.get("is_causal") is not False:
|
||||
raise NotImplementedError(f"Expected a Nemotron Embed model for {hf_arch}, but is_causal is not False")
|
||||
super().__init__(dir_model, *args, **kwargs)
|
||||
if self.hparams.get("model_type") == "mistral4":
|
||||
self.impl = Mistral3Model.Mistral4Model(*args, **kwargs)
|
||||
self.impl = Mistral3Model.Mistral4Model(dir_model, *args, **kwargs)
|
||||
elif self.hf_arch in ("Ministral3Model", "Mistral3Model"):
|
||||
self.impl = Mistral3Model.Mistral3EmbedNemotronModel(dir_model, *args, **kwargs)
|
||||
else:
|
||||
self.impl = Mistral3Model.Ministral3Model(*args, **kwargs)
|
||||
self.impl = Mistral3Model.Ministral3Model(dir_model, *args, **kwargs)
|
||||
|
||||
def set_vocab(self):
|
||||
self.impl.set_vocab()
|
||||
@@ -66,3 +93,13 @@ class Mistral3Model(TextModel):
|
||||
|
||||
def write(self):
|
||||
self.impl.write()
|
||||
|
||||
|
||||
@ModelBase.register("VoxtralRealtimeForConditionalGeneration")
|
||||
@ModelBase.example("mistralai/Voxtral-Mini-4B-Realtime-2602")
|
||||
class VoxtralRealtimeModel(LlamaModel):
|
||||
model_arch = gguf.MODEL_ARCH.MISTRAL3
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_realtime_delay_tokens(self.hparams["default_num_delay_tokens"])
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Iterable, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import ModelBase, gguf
|
||||
|
||||
from .mistral import MistralModel, MistralMoeModel
|
||||
|
||||
|
||||
@ModelBase.register("MistralLarge4ForConditionalGeneration")
|
||||
class MistralLarge4Model(MistralMoeModel):
|
||||
model_arch = gguf.MODEL_ARCH.MISTRAL_LARGE4
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
n_layer = self.hparams["num_hidden_layers"]
|
||||
dsa = self.hparams["dsa"]
|
||||
|
||||
def expand(pattern: str) -> list[str]:
|
||||
values = pattern.split(",")
|
||||
return [values[i % len(values)] for i in range(n_layer)]
|
||||
|
||||
self.layer_has_rope = [v == "rope" for v in expand(self.hparams.get("nope") or "rope")]
|
||||
self.layer_indexer_full = [v == "full" for v in expand(dsa.get("sharing") or "full")]
|
||||
|
||||
kv_sharing = self.hparams.get("kv_sharing") or {}
|
||||
if "source_layers_str" in kv_sharing:
|
||||
self.layer_kv_source = [int(v) for v in kv_sharing["source_layers_str"].split(",")]
|
||||
else:
|
||||
assert not kv_sharing, f"unsupported kv_sharing config: {kv_sharing}"
|
||||
self.layer_kv_source = list(range(n_layer))
|
||||
assert len(self.layer_kv_source) == n_layer
|
||||
|
||||
# note: the rope dim of the indexer must match the main attention
|
||||
assert dsa.get("rope_head_dim", -1) in (-1, self.hparams["qk_rope_head_dim"])
|
||||
assert dsa.get("match_nope", False), "indexer must follow the NoPE pattern of the main attention"
|
||||
assert dsa.get("ssmax_scaling", False), "indexer without SSMax is not supported"
|
||||
assert self.hparams.get("q_lora_rank") is None
|
||||
assert not self.hparams["yarn"].get("apply_scale", True), "only YaRN without magnitude scaling is supported"
|
||||
|
||||
def dequant_model(self):
|
||||
quant_config = self.hparams.get("quantization")
|
||||
if quant_config is not None:
|
||||
assert quant_config["qformat_weight"] == "fp8_e4m3"
|
||||
assert quant_config.get("qscheme_weight") == "BLOCK"
|
||||
self.hparams["quantization_config"] = {
|
||||
"quant_method": "fp8",
|
||||
"weight_block_size": [quant_config["block_size_out"], quant_config["block_size_in"]],
|
||||
}
|
||||
return super().dequant_model()
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
# skip the MistralMoeModel overrides, they apply the deepseek2 YaRN scaling
|
||||
super(MistralMoeModel, self).set_gguf_parameters()
|
||||
MistralModel.set_mistral_config(self.gguf_writer, self.hparams)
|
||||
|
||||
dsa = self.hparams["dsa"]
|
||||
self.gguf_writer.add_indexer_head_count(dsa["n_heads"])
|
||||
self.gguf_writer.add_indexer_key_length(dsa["head_dim"])
|
||||
self.gguf_writer.add_indexer_top_k(dsa["topk"])
|
||||
self.gguf_writer.add_indexer_types(self.layer_indexer_full)
|
||||
self.gguf_writer.add_layer_norm_eps(dsa.get("norm_eps", 1e-6))
|
||||
|
||||
self.gguf_writer.add_rope_pattern(self.layer_has_rope)
|
||||
self.gguf_writer.add_kv_source_map(self.layer_kv_source)
|
||||
self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SOFTMAX)
|
||||
self.gguf_writer.add_final_logit_softcapping(self.hparams["softmax_tanh_gating"])
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
name, gen = item
|
||||
|
||||
# exported separately with --dspark
|
||||
if name.startswith("mtp_"):
|
||||
return None
|
||||
|
||||
# experts need HF names for the merging logic in DeepseekV2Model
|
||||
# note: bare "layers.N.experts" comes from the NVFP4 quantized_layers config
|
||||
if ".experts." in name or name.endswith(".experts"):
|
||||
name = "model." + name.replace(".experts", ".mlp.experts")
|
||||
name = name.replace(".w1.", ".gate_proj.").replace(".w2.", ".down_proj.").replace(".w3.", ".up_proj.")
|
||||
|
||||
# skip the MistralMoeModel renaming, the qscale names are needed for block FP8 dequant
|
||||
return super(MistralMoeModel, cls).filter_tensors((name, gen))
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name == "lerope.alpha":
|
||||
# LeRoPE scales inv_freq by exp(alpha), ggml divides theta by the freq factors
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FREQS), torch.exp(-data_torch.float()))
|
||||
return
|
||||
|
||||
if bid is not None and name.endswith((".ssmax_scaling.s", ".ssmax_scaling.b")):
|
||||
tensor = gguf.MODEL_TENSOR.INDEXER_SSMAX if ".indexer." in name else gguf.MODEL_TENSOR.ATTN_SSMAX
|
||||
suffix = ".weight" if name.endswith(".s") else ".bias"
|
||||
yield (self.format_tensor_name(tensor, bid, suffix), data_torch.float())
|
||||
return
|
||||
|
||||
if bid is not None and ".indexer.q_input_norm." in name:
|
||||
suffix = ".weight" if name.endswith(".weight") else ".bias"
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.INDEXER_Q_NORM, bid, suffix), data_torch)
|
||||
return
|
||||
|
||||
if bid is not None and ".indexer." in name:
|
||||
name = "model." + name.replace(".attention.", ".self_attn.")
|
||||
|
||||
# NoPE layers have a larger non-rotary head, so wkv_b is split per layer
|
||||
if bid is not None and name.endswith(".wkv_b.weight"):
|
||||
n_head = self.hparams["num_attention_heads"]
|
||||
v_head_dim = self.hparams["v_head_dim"]
|
||||
qk_nope_head_dim = self.hparams["qk_nope_head_dim"]
|
||||
if not self.layer_has_rope[bid]:
|
||||
qk_nope_head_dim += self.hparams["qk_rope_head_dim"]
|
||||
|
||||
assert data_torch.shape[0] == n_head * (qk_nope_head_dim + v_head_dim)
|
||||
|
||||
kv_b = data_torch.view(n_head, qk_nope_head_dim + v_head_dim, data_torch.shape[-1])
|
||||
k_b, v_b = torch.split(kv_b, [qk_nope_head_dim, v_head_dim], dim=1)
|
||||
k_b = k_b.transpose(1, 2)
|
||||
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_K_B, bid), k_b)
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_V_B, bid), v_b)
|
||||
return
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
|
||||
class MistralLarge4DSparkModel(MistralLarge4Model):
|
||||
"""Mistral Large 4 DSpark draft (mtp_dspark.*) as a DFLASH GGUF, export with --dspark"""
|
||||
model_arch = gguf.MODEL_ARCH.DFLASH
|
||||
|
||||
_DSPARK_ROOT: dict[str, gguf.MODEL_TENSOR] = {
|
||||
"backbone.target_projection.weight": gguf.MODEL_TENSOR.FC,
|
||||
"backbone.target_norm.weight": gguf.MODEL_TENSOR.ENC_OUTPUT_NORM,
|
||||
"backbone.final_norm.weight": gguf.MODEL_TENSOR.OUTPUT_NORM,
|
||||
"backbone.mask_embedding": gguf.MODEL_TENSOR.DSPARK_MASK_EMBD,
|
||||
"autoregressive_head.embedding.weight": gguf.MODEL_TENSOR.DSPARK_MARKOV_W1,
|
||||
"autoregressive_head.output.weight": gguf.MODEL_TENSOR.DSPARK_MARKOV_W2,
|
||||
"autoregressive_head.joint_proj.weight": gguf.MODEL_TENSOR.DSPARK_JOINT_PROJ,
|
||||
"autoregressive_head.joint_proj.bias": gguf.MODEL_TENSOR.DSPARK_JOINT_PROJ,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
mtp = self.hparams.get("mtp") or {}
|
||||
dspark = mtp.get("dspark")
|
||||
if dspark is None:
|
||||
raise ValueError("Mistral Large 4 DSpark export needs the mtp.dspark config")
|
||||
assert dspark.get("head_type") == "rnn", f"unsupported DSpark head type: {dspark.get('head_type')}"
|
||||
assert dspark.get("draft_input") == "position_specific", f"unsupported DSpark draft input: {dspark.get('draft_input')}"
|
||||
|
||||
windows = {int(v) for v in (mtp.get("ragged_attention") or "").split(",") if v.strip()}
|
||||
assert len(windows) == 1, f"DSpark draft layers must share one sliding window, got {mtp.get('ragged_attention')}"
|
||||
|
||||
self._n_stage = int(mtp["n_layers"])
|
||||
self._depth = int(mtp["depth"])
|
||||
self._window = windows.pop()
|
||||
self._target_layers = [int(v) for v in mtp["target_activation_layers"].split(",")]
|
||||
|
||||
self.layer_has_rope = [True] * self._n_stage
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
name, gen = item
|
||||
if not name.startswith("mtp_dspark."):
|
||||
return None
|
||||
name = name.removeprefix("mtp_dspark.")
|
||||
if name.startswith("backbone.layers."):
|
||||
return name.removeprefix("backbone."), gen
|
||||
return f"dspark.{name}", gen
|
||||
|
||||
def set_vocab(self):
|
||||
super().set_vocab()
|
||||
# block uses mask_embd, mask token id is not used
|
||||
self.gguf_writer.add_mask_token_id(0)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
hp = self.hparams
|
||||
w = self.gguf_writer
|
||||
w.add_block_count(self._n_stage)
|
||||
w.add_context_length(hp["max_position_embeddings"])
|
||||
w.add_embedding_length(hp["hidden_size"])
|
||||
w.add_feed_forward_length(hp["intermediate_size"])
|
||||
w.add_head_count(hp["num_attention_heads"])
|
||||
w.add_head_count_kv(1)
|
||||
w.add_key_length(hp["kv_lora_rank"] + hp["qk_rope_head_dim"])
|
||||
w.add_value_length(hp["kv_lora_rank"])
|
||||
w.add_key_length_mla(hp["qk_nope_head_dim"] + hp["qk_rope_head_dim"])
|
||||
w.add_value_length_mla(hp["v_head_dim"])
|
||||
w.add_kv_lora_rank(hp["kv_lora_rank"])
|
||||
w.add_rope_dimension_count(hp["qk_rope_head_dim"])
|
||||
w.add_rope_freq_base(float(hp["rope_theta"]))
|
||||
w.add_layer_norm_rms_eps(hp["rms_norm_eps"])
|
||||
MistralModel.set_mistral_config(w, hp)
|
||||
w.add_sliding_window(self._window)
|
||||
w.add_final_logit_softcapping(hp["softmax_tanh_gating"])
|
||||
w.add_block_size(self._depth)
|
||||
# draft reads layer output = input of next layer
|
||||
w.add_target_layers([layer + 1 for layer in self._target_layers])
|
||||
w.add_sample_from_anchor(True)
|
||||
w.add_has_confidence_head(False)
|
||||
w.add_file_type(self.ftype)
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name.startswith("dspark."):
|
||||
key = name.removeprefix("dspark.")
|
||||
if key not in self._DSPARK_ROOT:
|
||||
raise ValueError(f"Unexpected DSpark tensor {name!r}")
|
||||
suffix = ".bias" if key.endswith(".bias") else ".weight"
|
||||
yield (self.format_tensor_name(self._DSPARK_ROOT[key], None, suffix), data_torch)
|
||||
return
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
@@ -94,7 +94,7 @@ class MuseGlimmerVisionModel(MmprojModel):
|
||||
assert self.hparams_vision is not None
|
||||
c = self.hparams_vision # enriched vision_config from get_vision_config()
|
||||
|
||||
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.MUSE_GLIMMER)
|
||||
self.gguf_writer.add_clip_vision_projector_type(gguf.VisionProjectorType.MUSE_GLIMMER)
|
||||
self.gguf_writer.add_vision_attention_layernorm_eps(float(c["layer_norm_eps"]))
|
||||
self.gguf_writer.add_vision_spatial_merge_size(int(c["merge_size"]))
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import ModelBase, MmprojModel, TextModel, gguf
|
||||
from .muse_glimmer import MuseGlimmerVisionModel
|
||||
|
||||
|
||||
# The reference clamps the squared stream coefficient with this floor.
|
||||
RESIDUAL_GATE_EPS = 1e-3
|
||||
|
||||
# Block norms hold a delta around one; the output norm holds an absolute gain.
|
||||
OFFSET_NORM_SUFFIXES = (
|
||||
"input_layernorm.weight",
|
||||
"pre_feedforward_layernorm.weight",
|
||||
"post_feedforward_layernorm.weight",
|
||||
"pre_expert_norm.weight",
|
||||
"post_expert_norm.weight",
|
||||
)
|
||||
|
||||
|
||||
def _unpermute_for_rope(tensor: "Tensor", n_heads: int) -> "Tensor":
|
||||
"""Invert transformers' `_permute_for_rope`: HF stores Q/K in rotate_half layout,
|
||||
llama.cpp consumes the interleaved (NORM) layout. Vision Q/K carry a bias too."""
|
||||
if tensor.ndim == 1:
|
||||
dim1, = tensor.shape
|
||||
return tensor.view(n_heads, 2, dim1 // n_heads // 2).transpose(1, 2).reshape(dim1)
|
||||
dim1, dim2 = tensor.shape
|
||||
return tensor.view(n_heads, 2, dim1 // n_heads // 2, dim2).transpose(1, 2).reshape(dim1, dim2)
|
||||
|
||||
|
||||
@ModelBase.register("MuseSparkForConditionalGeneration")
|
||||
@ModelBase.example("meta-models/Muse-Spark-Open-816B-A42B")
|
||||
class MuseSparkModel(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.MUSE_SPARK
|
||||
|
||||
def set_vocab(self):
|
||||
self._set_vocab_gpt2()
|
||||
|
||||
from transformers import AutoTokenizer
|
||||
tok = AutoTokenizer.from_pretrained(self.dir_model)
|
||||
eot_id = tok.convert_tokens_to_ids("<|eot|>") # ty: ignore[unresolved-attribute]
|
||||
if isinstance(eot_id, int) and eot_id >= 0:
|
||||
self.gguf_writer.add_eot_token_id(eot_id)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
hparams = self.hparams
|
||||
|
||||
# MetaP resolves to two scalars
|
||||
# The output multiplier lands here and the attention scale rides on the synthesized QK-norm weights
|
||||
self.gguf_writer.add_logit_scale(hparams["output_multiplier"])
|
||||
self.gguf_writer.add_final_logit_softcapping(hparams["final_logit_softcapping"])
|
||||
|
||||
self.gguf_writer.add_post_norm_eps(hparams["post_norm_eps"])
|
||||
|
||||
# Only the sliding layers carry rope, so the graph takes it from the pattern
|
||||
sliding = [t == "sliding_attention" for t in hparams["layer_types"]]
|
||||
theta = hparams["rope_parameters"]["rope_theta"]
|
||||
assert sliding == [bool(t) for t in hparams["layer_rope_theta"]]
|
||||
assert all(t in (0, theta) for t in hparams["layer_rope_theta"])
|
||||
assert all(t == "sparse" for t in hparams["mlp_layer_types"])
|
||||
|
||||
self.gguf_writer.add_sliding_window(hparams["sliding_window"])
|
||||
self.gguf_writer.add_rope_freq_base_swa(theta)
|
||||
self.gguf_writer.add_sliding_window_pattern(sliding)
|
||||
|
||||
# Experts live in a latent space narrower than the residual stream.
|
||||
self.gguf_writer.add_moe_latent_size(hparams["moe_hidden_size"])
|
||||
self.gguf_writer.add_expert_feed_forward_length(hparams["moe_intermediate_size"])
|
||||
self.gguf_writer.add_expert_weights_scale(hparams["routed_scaling_factor"])
|
||||
self.gguf_writer.add_expert_weights_norm(True)
|
||||
# the config carries no score function, the reference hardcodes sigmoid
|
||||
self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SIGMOID)
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name.endswith(OFFSET_NORM_SUFFIXES):
|
||||
data_torch = data_torch + 1.0
|
||||
|
||||
if name.endswith(".self_attn.q_proj.weight"):
|
||||
assert bid is not None
|
||||
n_head = self.hparams["num_attention_heads"]
|
||||
head_dim = self.hparams["head_dim"]
|
||||
|
||||
yield self.map_tensor_name(name), _unpermute_for_rope(data_torch, n_head)
|
||||
|
||||
# Synthesize QK-norm to carry the MetaP scale so the graph keeps the plain 1 / sqrt(head_dim)
|
||||
ones = torch.ones(head_dim, dtype=torch.float32)
|
||||
q_norm = ones * self.hparams["qk_scale_factor"]
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_Q_NORM, bid), q_norm
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_K_NORM, bid), ones
|
||||
return
|
||||
|
||||
if name.endswith(".self_attn.k_proj.weight"):
|
||||
n_head_kv = self.hparams["num_key_value_heads"]
|
||||
yield self.map_tensor_name(name), _unpermute_for_rope(data_torch, n_head_kv)
|
||||
return
|
||||
|
||||
# gate_up_proj fuses both projections over the output dim, gate first.
|
||||
if name.endswith(".mlp.experts.gate_up_proj"):
|
||||
assert bid is not None
|
||||
n_ff = data_torch.shape[1] // 2
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE_EXP, bid), data_torch[:, :n_ff, :]
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP_EXP, bid), data_torch[:, n_ff:, :]
|
||||
return
|
||||
|
||||
if name.endswith(".mlp.experts.down_proj"):
|
||||
assert bid is not None
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.FFN_DOWN_EXP, bid), data_torch
|
||||
return
|
||||
|
||||
# The router scales its logits by the MetaP output multiplier before the sigmoid
|
||||
# Fold it into the weight to keep a plain matmul
|
||||
if name.endswith(".mlp.gate.weight"):
|
||||
data_torch = data_torch.float() * self.hparams["output_multiplier"]
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
return
|
||||
|
||||
# Norm preserving residual gate: alpha scales the stream, beta the branch, with alpha^2 + beta^2 = 1
|
||||
# Both follow from the weight, so resolve them here
|
||||
if name.endswith("_residual_gate.gate"):
|
||||
assert bid is not None
|
||||
attn = "post_attention" in name
|
||||
|
||||
w = data_torch.float() / self.hparams["residual_gate_temperature"]
|
||||
beta = torch.sigmoid(w)
|
||||
alpha_sq = torch.sigmoid(-w) * (1.0 + beta)
|
||||
alpha = torch.sqrt(torch.clamp(alpha_sq, min=RESIDUAL_GATE_EPS))
|
||||
|
||||
t_alpha = gguf.MODEL_TENSOR.ATTN_RESID_ALPHA if attn else gguf.MODEL_TENSOR.FFN_RESID_ALPHA
|
||||
t_beta = gguf.MODEL_TENSOR.ATTN_RESID_BETA if attn else gguf.MODEL_TENSOR.FFN_RESID_BETA
|
||||
yield self.format_tensor_name(t_alpha, bid), alpha
|
||||
yield self.format_tensor_name(t_beta, bid), beta
|
||||
return
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
|
||||
@ModelBase.register("MuseSparkForConditionalGeneration")
|
||||
@ModelBase.example("meta-models/Muse-Spark-Open-816B-A42B")
|
||||
class MuseSparkVisionModel(MuseGlimmerVisionModel):
|
||||
has_vision_encoder = True # same vision encoder as Muse Glimmer
|
||||
has_audio_encoder = True
|
||||
|
||||
def get_audio_config(self) -> dict[str, Any]:
|
||||
c = self.global_config["audio_config"]
|
||||
# the base class wants one width and one layer count: use the last stage width and the total number of conv blocks
|
||||
return {
|
||||
**c,
|
||||
"hidden_size": c["hidden_sizes"][-1],
|
||||
"intermediate_size": int(c["hidden_sizes"][-1] * c["mlp_ratio"]),
|
||||
"num_attention_heads": 1,
|
||||
"num_hidden_layers": sum(c["depths"]),
|
||||
}
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
|
||||
assert self.hparams_audio
|
||||
a = self.hparams_audio
|
||||
|
||||
self.gguf_writer.add_clip_audio_projector_type(gguf.VisionProjectorType.MUSE_SPARK_AUDIO)
|
||||
# the input is the raw waveform, one channel takes the place of the mel bins
|
||||
self.gguf_writer.add_audio_num_mel_bins(1)
|
||||
self.gguf_writer.add_audio_attention_layernorm_eps(float(a["rms_norm_eps"]))
|
||||
self.gguf_writer.add_audio_conv_kernel_size(int(a["conv_kernel_size"]))
|
||||
self.gguf_writer.add_audio_hidden_sizes([int(x) for x in a["hidden_sizes"]])
|
||||
self.gguf_writer.add_audio_patch_sizes([int(x) for x in a["patch_sizes"]])
|
||||
self.gguf_writer.add_audio_depths([int(x) for x in a["depths"]])
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item):
|
||||
name, gen = item
|
||||
if name.startswith("model.audio_tower."):
|
||||
return MmprojModel.filter_tensors((name, gen))
|
||||
return super().filter_tensors((name, gen))
|
||||
|
||||
_AUDIO_HEAD = {
|
||||
"model.audio_tower.norm": gguf.MODEL_TENSOR.A_PRE_NORM,
|
||||
"model.audio_tower.proj": gguf.MODEL_TENSOR.A_MMPROJ_FC,
|
||||
"model.audio_tower.post_proj_norm": gguf.MODEL_TENSOR.A_POST_NORM,
|
||||
}
|
||||
|
||||
def _audio_tensor(self, data_torch, name, suffix):
|
||||
assert self.hparams_audio
|
||||
|
||||
stem = name[: -len(suffix) - 1]
|
||||
|
||||
if stem in self._AUDIO_HEAD:
|
||||
return self.format_tensor_name(self._AUDIO_HEAD[stem], suffix="." + suffix), data_torch
|
||||
|
||||
parts = stem.split(".")
|
||||
stage = int(parts[3])
|
||||
|
||||
if parts[4] == "patch_embedder":
|
||||
key = (gguf.MODEL_TENSOR.A_ENC_PATCH_PROJ if parts[5] == "proj"
|
||||
else gguf.MODEL_TENSOR.A_ENC_PATCH_NORM)
|
||||
return self.format_tensor_name(key, bid=stage, suffix="." + suffix), data_torch
|
||||
|
||||
# conv blocks are numbered per stage upstream and flat here
|
||||
depths = [int(d) for d in self.hparams_audio["depths"]]
|
||||
bid = sum(depths[:stage]) + int(parts[5])
|
||||
tail = ".".join(parts[6:])
|
||||
key = {
|
||||
"conv": gguf.MODEL_TENSOR.A_ENC_CONV1D,
|
||||
"norm": gguf.MODEL_TENSOR.A_ENC_CONV1D_NORM,
|
||||
"mlp.fc1": gguf.MODEL_TENSOR.A_ENC_FFN_UP,
|
||||
"mlp.fc2": gguf.MODEL_TENSOR.A_ENC_FFN_DOWN,
|
||||
}[tail]
|
||||
return self.format_tensor_name(key, bid=bid, suffix="." + suffix), data_torch
|
||||
|
||||
def modify_tensors(self, data_torch, name, bid):
|
||||
if name.startswith("model.audio_tower."):
|
||||
yield self._audio_tensor(data_torch, name, name.rpartition(".")[2])
|
||||
return
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
+157
-16
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Iterable, TYPE_CHECKING
|
||||
import re
|
||||
from typing import Any, Callable, Iterable, Sequence, TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
@@ -89,19 +90,22 @@ class LiLiCorrDraftModel(DFlashModel):
|
||||
self.gguf_writer.add_lilicorr_vector_eps(float(dflash_config["lilicorr_vector_eps"]))
|
||||
self.gguf_writer.add_lilicorr_logit_scale(float(dflash_config["lilicorr_logit_scale"]))
|
||||
|
||||
def map_tensor_name(self, name: str, try_suffixes: Sequence[str] = (".weight", ".bias")) -> str:
|
||||
if name.startswith("model.lilicorr.layers."):
|
||||
parts = name.split(".", 4)
|
||||
if not parts[4].startswith("attn.in_proj."):
|
||||
parts[3] = str(self.block_count + int(parts[3]))
|
||||
name = ".".join(parts)
|
||||
elif name.startswith("model.lilicorr.feature_norm."):
|
||||
feature_bid = self.block_count + self.lilicorr_geometry["num_layers"]
|
||||
name = name.replace("model.lilicorr.feature_norm.", f"model.lilicorr.feature_mlp.{feature_bid}.norm.")
|
||||
elif name.startswith("model.lilicorr.feature_mlp."):
|
||||
feature_bid = self.block_count + self.lilicorr_geometry["num_layers"]
|
||||
name = name.replace("model.lilicorr.feature_mlp.", f"model.lilicorr.feature_mlp.{feature_bid}.")
|
||||
return super().map_tensor_name(name, try_suffixes)
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name.startswith("model.lilicorr."):
|
||||
if name.startswith("model.lilicorr.layers."):
|
||||
parts = name.split(".", 4)
|
||||
if not parts[4].startswith("attn.in_proj."):
|
||||
parts[3] = str(self.block_count + int(parts[3]))
|
||||
name = ".".join(parts)
|
||||
elif name.startswith("model.lilicorr.feature_norm."):
|
||||
feature_bid = self.block_count + self.lilicorr_geometry["num_layers"]
|
||||
name = name.replace("model.lilicorr.feature_norm.", f"model.lilicorr.feature_mlp.{feature_bid}.norm.")
|
||||
elif name.startswith("model.lilicorr.feature_mlp."):
|
||||
feature_bid = self.block_count + self.lilicorr_geometry["num_layers"]
|
||||
name = name.replace("model.lilicorr.feature_mlp.", f"model.lilicorr.feature_mlp.{feature_bid}.")
|
||||
yield self.map_tensor_name(name), data_torch
|
||||
return
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
@@ -251,15 +255,134 @@ class NemotronNanoV2VLModel(MmprojModel):
|
||||
yield mapped_name, tensor
|
||||
|
||||
|
||||
@ModelBase.register("NemotronH_Omni_Reasoning_V3")
|
||||
class NemotronOmniVLModel(NemotronNanoV2VLModel):
|
||||
_omni_tensor_prefixes = (
|
||||
"vision_model.embeddings.",
|
||||
"vision_model.encoder.layer.",
|
||||
"vision_projector.mlp1.",
|
||||
)
|
||||
|
||||
def get_vision_config(self) -> dict[str, Any] | None:
|
||||
vision_config = dict(self.global_config["vision_config"])
|
||||
vision_config["intermediate_size"] = int(vision_config["hidden_size"] * vision_config["mlp_ratio"])
|
||||
vision_config["image_size"] = 512 # warmup only; the full position table is resized at runtime
|
||||
return vision_config
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
preprocessor_config = self.preprocessor_config
|
||||
|
||||
preprocessor_config["image_mean"] = preprocessor_config["norm_mean"]
|
||||
preprocessor_config["image_std"] = preprocessor_config["norm_std"]
|
||||
|
||||
MmprojModel.set_gguf_parameters(self)
|
||||
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.NEMOTRON_OMNI)
|
||||
self.gguf_writer.add_vision_attention_layernorm_eps(1e-6)
|
||||
self.gguf_writer.add_vision_use_gelu(True)
|
||||
downsample_ratio = self.global_config.get("downsample_ratio", 0.5)
|
||||
self.gguf_writer.add_vision_projector_scale_factor(int(1.0 / downsample_ratio))
|
||||
patch_area = self.hparams["patch_size"] ** 2
|
||||
self.gguf_writer.add_vision_min_pixels(preprocessor_config["min_num_patches"] * patch_area)
|
||||
self.gguf_writer.add_vision_max_pixels(preprocessor_config["max_num_patches"] * patch_area)
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
name, gen = item
|
||||
|
||||
# Transformers can rename RADIO and projector tensors in Omni checkpoints.
|
||||
# Accept both checkpoint layouts.
|
||||
if name == "vision_model.summary_idxs":
|
||||
# This buffer is only used by RadioModel.summary. The multimodal
|
||||
# projector consumes RadioModel.features for both images and video.
|
||||
return None
|
||||
if name.startswith(cls._omni_tensor_prefixes):
|
||||
return name, gen
|
||||
|
||||
if "radio_model.model.patch_generator.video_embedder" in name or name.startswith("vision_projector.vision_final_layernorm."):
|
||||
return MmprojModel.filter_tensors((name, gen))
|
||||
|
||||
return super().filter_tensors(item)
|
||||
|
||||
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
||||
if new_name in ("v.position_embd.weight", "v.class_embd"):
|
||||
return gguf.GGMLQuantizationType.F32
|
||||
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name.startswith(self._omni_tensor_prefixes):
|
||||
if name == "vision_model.embeddings.video_patch_projection.weight":
|
||||
data_torch = data_torch.reshape(
|
||||
self.hparams["hidden_size"],
|
||||
6,
|
||||
self.hparams["patch_size"],
|
||||
self.hparams["patch_size"],
|
||||
)
|
||||
yield gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".weight.1", data_torch
|
||||
return
|
||||
|
||||
if name.startswith("vision_projector.mlp1."):
|
||||
projector_mapping = {
|
||||
"vision_projector.mlp1.norm.weight": "mlp1.0.weight",
|
||||
"vision_projector.mlp1.linear1.weight": "mlp1.1.weight",
|
||||
"vision_projector.mlp1.linear2.weight": "mlp1.3.weight",
|
||||
}
|
||||
name = projector_mapping[name]
|
||||
elif name == "vision_model.embeddings.patch_projection.weight":
|
||||
data_torch = data_torch.reshape(
|
||||
self.hparams["hidden_size"],
|
||||
3,
|
||||
self.hparams["patch_size"],
|
||||
self.hparams["patch_size"],
|
||||
)
|
||||
elif name == "vision_model.embeddings.position_embedding":
|
||||
data_torch = data_torch.squeeze(0)
|
||||
name += ".weight"
|
||||
elif ".layer_scale" in name and name.endswith(".lambda1"):
|
||||
name = name.removesuffix("lambda1") + "weight"
|
||||
|
||||
yield from MmprojModel.modify_tensors(self, data_torch, name, bid)
|
||||
return
|
||||
|
||||
if "patch_generator.pos_embed" in name:
|
||||
# Keep the full table for runtime interpolation.
|
||||
yield from MmprojModel.modify_tensors(self, data_torch, name, bid)
|
||||
return
|
||||
|
||||
if "patch_generator.video_embedder" in name:
|
||||
patch_size = self.hparams["patch_size"]
|
||||
n_embd = self.hparams["hidden_size"]
|
||||
temporal_patch_size = self.global_config["video_temporal_patch_size"]
|
||||
if temporal_patch_size != 2:
|
||||
raise ValueError("Current implementation only supports a temporal patch size of 2")
|
||||
data_torch = data_torch.reshape(n_embd, temporal_patch_size * 3, patch_size, patch_size)
|
||||
new_name = gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".weight.1"
|
||||
yield new_name, data_torch
|
||||
return
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
|
||||
@ModelBase.register("NemotronForCausalLM")
|
||||
@ModelBase.example("nvidia/Minitron-4B-Base")
|
||||
class NemotronModel(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.NEMOTRON
|
||||
|
||||
def set_vocab(self):
|
||||
self._set_vocab_sentencepiece()
|
||||
self.gguf_writer.add_pad_token_id(0)
|
||||
self.gguf_writer.add_unk_token_id(1)
|
||||
if (self.dir_model / "tokenizer.model").exists():
|
||||
self._set_vocab_sentencepiece()
|
||||
self.gguf_writer.add_pad_token_id(0)
|
||||
self.gguf_writer.add_unk_token_id(1)
|
||||
else:
|
||||
# SPM-style BPE (Metaspace + byte fallback): mark <0xXX> tokens as BYTE so they decode to raw bytes
|
||||
tokens, toktypes, tokpre = self.get_vocab_base()
|
||||
toktypes = [gguf.TokenType.BYTE if re.fullmatch(r"<0x[0-9A-F]{2}>", tok) else typ for tok, typ in zip(tokens, toktypes)]
|
||||
self.gguf_writer.add_tokenizer_model("gpt2")
|
||||
self.gguf_writer.add_tokenizer_pre(tokpre)
|
||||
self.gguf_writer.add_token_list(tokens)
|
||||
self.gguf_writer.add_token_types(toktypes)
|
||||
|
||||
special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
|
||||
special_vocab.add_to_gguf(self.gguf_writer)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
@@ -273,7 +396,10 @@ class NemotronModel(TextModel):
|
||||
rot_pct = self.rope_parameters["partial_rotary_factor"]
|
||||
n_embd = self.find_hparam(["hidden_size", "n_embd"])
|
||||
n_head = self.find_hparam(["num_attention_heads", "n_head"])
|
||||
self.gguf_writer.add_rope_dimension_count(int(rot_pct * n_embd) // n_head)
|
||||
# Use explicit head_dim when available; fallback keeps standard Nemotron working.
|
||||
# Without this, models where n_head * head_dim != n_embd (e.g. 48 heads, head_dim=128) produce the wrong rope dimension count.
|
||||
head_dim = self.hparams.get("head_dim") or n_embd // n_head
|
||||
self.gguf_writer.add_rope_dimension_count(int(rot_pct * head_dim))
|
||||
|
||||
# * RopeScaling for Nemotron
|
||||
factor = self.hparams.get("factor") or self.rope_parameters.get("factor")
|
||||
@@ -307,6 +433,11 @@ class NemotronHModel(GraniteHybridModel):
|
||||
_ATTN_LAYER_TYPES = {"attention", "full_attention"}
|
||||
_MLP_LAYER_TYPES = {"moe"}
|
||||
|
||||
def _map_fp8_weight_names(self, name: str) -> tuple[str, ...]:
|
||||
if ".mixer.experts." in name:
|
||||
name = name.replace("backbone.", "model.").replace(".mixer.experts.", ".mlp.experts.")
|
||||
return super()._map_fp8_weight_names(name)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# We have to determine the correct model architecture (MoE vs non-MoE) before
|
||||
# calling the parent __init__. This is because the parent constructor
|
||||
@@ -382,6 +513,8 @@ class NemotronHModel(GraniteHybridModel):
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
name, gen = item
|
||||
if name.startswith("language_model."):
|
||||
name = name[len("language_model."):]
|
||||
if name.startswith("mtp."):
|
||||
# --no-mtp: drop the MTP head entirely
|
||||
if cls.no_mtp:
|
||||
@@ -531,6 +664,14 @@ class NemotronHModel(GraniteHybridModel):
|
||||
"mtp.layers.1.final_layernorm.weight": "model.layers.{bid}.shared_head.norm.weight",
|
||||
}
|
||||
|
||||
def _generate_nvfp4_tensors(self):
|
||||
if self._mtp_bid is not None:
|
||||
for name in list(self.model_tensors):
|
||||
if name.startswith("mtp.layers.1.mixer"):
|
||||
new_name = f"backbone.layers.{self._mtp_bid}.{name.split('.', 3)[3]}"
|
||||
self.model_tensors[new_name] = self.model_tensors.pop(name)
|
||||
super()._generate_nvfp4_tensors()
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
# mtp.layers.0: NextN input fusion + attention
|
||||
# mtp.layers.1: MoE + final head norm
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import json
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Iterable, TYPE_CHECKING
|
||||
from typing import Any, Iterable, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
@@ -35,6 +35,7 @@ def _load_pplx_decider_hparams(dir_model: Path) -> dict[str, Any]:
|
||||
|
||||
@ModelBase.register("PplxDeciderModel")
|
||||
@ModelBase.example("perplexity-ai/pplx-decider-v1-27b")
|
||||
@ModelBase.example("FINAL-Bench/Darwin-27B-ZTC-v5")
|
||||
class PplxDeciderModel(Qwen3_5TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.QWEN35
|
||||
no_mtp = True # the checkpoint has no MTP head
|
||||
@@ -77,14 +78,6 @@ class PplxDeciderModel(Qwen3_5TextModel):
|
||||
for name in ("choice", "score", "noul"):
|
||||
self.gguf_writer.add_decision_temperature(name, self.hparams["decision"]["temperature"])
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
||||
name, gen = item
|
||||
# the checkpoint is the bare backbone, its text tensors have no "model." prefix
|
||||
if name.startswith("language_model."):
|
||||
name = "model." + name
|
||||
return super().filter_tensors((name, gen))
|
||||
|
||||
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
||||
yield from super().generate_extra_tensors()
|
||||
from safetensors.torch import load_file
|
||||
@@ -92,7 +85,7 @@ class PplxDeciderModel(Qwen3_5TextModel):
|
||||
# the readout has one row per option label, store it as an LM head that is zero for the other tokens
|
||||
readout = load_file(self.dir_model / "readout.safetensors")["weight"]
|
||||
token_ids = self.hparams["decision"]["token_ids"]
|
||||
n_vocab = self.hparams["text_config"]["vocab_size"]
|
||||
n_vocab = self.hparams["vocab_size"]
|
||||
assert readout.shape[0] == len(token_ids) == len(set(token_ids))
|
||||
lm_head = torch.zeros(n_vocab, readout.shape[1], dtype=readout.dtype)
|
||||
lm_head[token_ids] = readout
|
||||
|
||||
@@ -59,6 +59,10 @@ class Qwen2Model(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.QWEN2
|
||||
|
||||
def set_vocab(self):
|
||||
# HuggingFaceBio/Carbon-14B: Qwen3 with the HybridDNATokenizer (Qwen3 BPE plus DNA 6-mers)
|
||||
if self._is_hybriddna():
|
||||
self._set_vocab_hybriddna()
|
||||
return
|
||||
try:
|
||||
self._set_vocab_sentencepiece()
|
||||
except FileNotFoundError:
|
||||
@@ -79,6 +83,15 @@ class Qwen2Model(TextModel):
|
||||
class Qwen2MoeModel(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.QWEN2MOE
|
||||
|
||||
def _map_fp8_weight_names(self, name: str) -> tuple[str, ...]:
|
||||
if name.removesuffix(".weight").endswith(".mlp.experts.gate_up_proj"):
|
||||
bid = next(int(part) for part in self.map_tensor_name(name).split(".") if part.isdecimal())
|
||||
return (
|
||||
self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE_EXP, bid),
|
||||
self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP_EXP, bid),
|
||||
)
|
||||
return super()._map_fp8_weight_names(name)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
if (moe_intermediate_size := self.hparams.get("moe_intermediate_size")) is not None:
|
||||
@@ -253,6 +266,11 @@ class Qwen3Model(Qwen2Model):
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
return
|
||||
|
||||
# output embedding projection (voyage-4-nano)
|
||||
if name == "linear.weight":
|
||||
self.gguf_writer.add_embedding_length_out(data_torch.shape[0])
|
||||
name = "dense_2_out.weight"
|
||||
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
|
||||
@@ -373,6 +391,16 @@ class _QwenMtpMixin:
|
||||
class Qwen3NextModel(_QwenMtpMixin, Qwen2MoeModel):
|
||||
model_arch = gguf.MODEL_ARCH.QWEN3NEXT
|
||||
|
||||
def _map_fp8_weight_names(self, name: str) -> tuple[str, ...]:
|
||||
if name.endswith(".linear_attn.in_proj_qkvz.weight"):
|
||||
bid = next(int(part) for part in self.map_tensor_name(name).split(".") if part.isdecimal())
|
||||
# Both split projections keep the original per-tensor scales.
|
||||
return (
|
||||
self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_QKV, bid),
|
||||
self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_GATE, bid),
|
||||
)
|
||||
return super()._map_fp8_weight_names(name)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_ssm_conv_kernel(self.hparams["linear_conv_kernel_dim"])
|
||||
@@ -681,6 +709,52 @@ class Qwen3_5TextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase):
|
||||
self._try_set_pooling_type()
|
||||
|
||||
|
||||
@ModelBase.register("MiniCPMOCRForSequenceClassification")
|
||||
class MiniCPMOCRSeqClsModel(Qwen3_5TextModel):
|
||||
# OCR consistency judge: Qwen3.5 VL trunk with a value head read at the last token
|
||||
# the class scores are [inconsistent, consistent], the softmax temperature is 1
|
||||
model_arch = gguf.MODEL_ARCH.QWEN35
|
||||
no_mtp = True # the checkpoint has no MTP head
|
||||
|
||||
CLASS_LABELS = ["inconsistent", "consistent"]
|
||||
|
||||
def set_vocab(self):
|
||||
super().set_vocab()
|
||||
self.gguf_writer.add_chat_template([{"name": "systemone", "template": self._systemone_template()}])
|
||||
|
||||
def _systemone_template(self) -> str:
|
||||
# the prompt of the training data, the two images are the original page and the one
|
||||
# rendered from the OCR text; the task is fixed, there is nothing to fill in
|
||||
# the trailing newlines are in an expression, so the template does not drop them
|
||||
return (
|
||||
"<|im_start|>user\n{{ images[0] }}\nOCR文本:\n{{ state }}\n{{ images[1] }}\n"
|
||||
"第一张图是原图,第二张图是上述 OCR 文本渲染得到的图片。\n"
|
||||
"请判断该 OCR 文本及其对应渲染图是否与原图内容一致。\n"
|
||||
"这是一个二分类任务:一致标为 1,不一致标为 0。"
|
||||
"{{ '<|im_end|>\\n<|im_start|>assistant\\n<think>\\n\\n</think>\\n\\n' }}"
|
||||
)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
|
||||
# the config.json of the checkpoint keeps the one of the base model, it has no num_labels / id2label
|
||||
if "score.weight" not in self.model_tensors:
|
||||
raise ValueError("the checkpoint has no score.weight classification head")
|
||||
|
||||
self.gguf_writer.add_pooling_type(gguf.PoolingType.LAST)
|
||||
self.gguf_writer.add_embedding_length_out(len(self.CLASS_LABELS))
|
||||
self.gguf_writer.add_classifier_output_labels(self.CLASS_LABELS)
|
||||
self.gguf_writer.add_decision_type(gguf.DecisionType.MINICPM_OCR_JEV)
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name == "score.weight":
|
||||
if data_torch.shape[0] != len(self.CLASS_LABELS):
|
||||
raise ValueError(f"score.weight must have {len(self.CLASS_LABELS)} rows, got {data_torch.shape[0]}")
|
||||
yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.CLS_OUT] + ".weight", data_torch)
|
||||
return
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
|
||||
|
||||
def _is_openjev_checkpoint(dir_model: Path) -> bool:
|
||||
return (dir_model / "helper" / "shim.py").is_file() and (dir_model / "config.json").is_file()
|
||||
|
||||
|
||||
@@ -144,6 +144,16 @@ class Qwen3VLVisionModel(MmprojModel):
|
||||
yield from MmprojModel.modify_tensors(self, data_torch, name, bid)
|
||||
|
||||
|
||||
@ModelBase.register("MiniCPMOCRForSequenceClassification")
|
||||
class MiniCPMOCRSeqClsVisionModel(Qwen3VLVisionModel):
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
# trained with the image budget of its processor, wider than the mtmd default of Qwen-VL
|
||||
size = self.preprocessor_config["size"]
|
||||
self.gguf_writer.add_vision_min_pixels(int(size["shortest_edge"]))
|
||||
self.gguf_writer.add_vision_max_pixels(int(size["longest_edge"]))
|
||||
|
||||
|
||||
@ModelBase.register("Qwen3OmniMoeForConditionalGeneration")
|
||||
@ModelBase.example("Qwen/Qwen3-Omni-30B-A3B-Instruct")
|
||||
class Qwen3OmniMmprojModel(Qwen3VLVisionModel, Qwen25AudioModel):
|
||||
|
||||
+54
-15
@@ -41,6 +41,8 @@ class Qwen4ExpTextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase):
|
||||
self._ple_shards: dict[int, str] = {}
|
||||
self._ple_row_dim: int | None = None
|
||||
self._mtp_fc: dict[str, Tensor] = {}
|
||||
# halves of the merged weights, keyed by (bid, merged tensor), see _merge_shared_input
|
||||
self._merge_parts: dict[tuple[int, gguf.MODEL_TENSOR], dict[int, Tensor]] = {}
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item):
|
||||
@@ -131,44 +133,79 @@ class Qwen4ExpTextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase):
|
||||
# int64 hash constants must stay exact; 1-D tensors force F32, so use KV
|
||||
if name.endswith("ple_embedding.layer_multipliers"):
|
||||
self._ple_multipliers = [int(x) for x in data_torch.tolist()]
|
||||
return []
|
||||
return
|
||||
if name.endswith("ple_embedding.ngram_heads_offsets"):
|
||||
self._ple_head_offsets = [int(x) for x in data_torch.tolist()]
|
||||
return []
|
||||
return
|
||||
if name.endswith("ple_embedding.ngram_heads_vocab_sizes"):
|
||||
self._ple_head_vocab_sizes = [int(x) for x in data_torch.tolist()]
|
||||
return []
|
||||
return
|
||||
|
||||
if ".ngram_embedding.shard_" in name:
|
||||
return self._place_ple_shard(data_torch, name)
|
||||
yield from self._place_ple_shard(data_torch, name)
|
||||
return
|
||||
|
||||
# eh_proj([e ; h_s]) = fc_embedding(e) + fc_hidden(h_s) for every hc stream s
|
||||
if name.endswith((".nextn_fc_embedding.weight", ".nextn_fc_hidden.weight")):
|
||||
self._mtp_fc[name.rsplit(".", 2)[1]] = data_torch
|
||||
if len(self._mtp_fc) < 2:
|
||||
return []
|
||||
return
|
||||
eh = torch.cat([self._mtp_fc.pop("nextn_fc_embedding"), self._mtp_fc.pop("nextn_fc_hidden")], dim=1)
|
||||
return [(self.format_tensor_name(gguf.MODEL_TENSOR.NEXTN_EH_PROJ, bid, ".weight"), eh)]
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.NEXTN_EH_PROJ, bid, ".weight"), eh)
|
||||
return
|
||||
|
||||
# one projection feeds indexer q and k; split it, as minimax-m3 does
|
||||
if ".indexer.index_qk_proj.weight" in name:
|
||||
n_q = self.hparams["indexer_n_heads"] * self.hparams["indexer_head_dim"]
|
||||
q = data_torch[:n_q]
|
||||
k = data_torch[n_q:]
|
||||
return [
|
||||
(self.format_tensor_name(gguf.MODEL_TENSOR.INDEXER_Q_PROJ, bid, ".weight"), q),
|
||||
(self.format_tensor_name(gguf.MODEL_TENSOR.INDEXER_K_PROJ, bid, ".weight"), k),
|
||||
]
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.INDEXER_Q_PROJ, bid, ".weight"), data_torch[:n_q])
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.INDEXER_K_PROJ, bid, ".weight"), data_torch[n_q:])
|
||||
return
|
||||
|
||||
# Gemma zero-centred gammas the inherited norm.weight rule misses
|
||||
if name.endswith((".ple.norm_key.weight", ".ple.norm_query.weight", ".ple.norm_conv.weight",
|
||||
".indexer.q_layernorm.weight", ".indexer.k_layernorm.weight")):
|
||||
return [(self.map_tensor_name(name), data_torch + 1)]
|
||||
yield (self.map_tensor_name(name), data_torch + 1)
|
||||
return
|
||||
|
||||
if name.endswith(".ple.conv1d.weight"):
|
||||
return [(self.map_tensor_name(name), data_torch.squeeze())]
|
||||
yield (self.map_tensor_name(name), data_torch.squeeze())
|
||||
return
|
||||
|
||||
return super().modify_tensors(data_torch, name, bid)
|
||||
for new_name, data in super().modify_tensors(data_torch, name, bid):
|
||||
yield from self._merge_shared_input(new_name, data, bid)
|
||||
|
||||
# matmuls that read the same input, written as one weight [a | b] so the runtime does one matmul:
|
||||
# (merged tensor, (a, b), linear-attention layers only)
|
||||
_SHARED_INPUT_MERGES = (
|
||||
(gguf.MODEL_TENSOR.HC_ATTN_DOWN_INJECT, (gguf.MODEL_TENSOR.HC_ATTN_DOWN, gguf.MODEL_TENSOR.HC_ATTN_INJECT), False),
|
||||
(gguf.MODEL_TENSOR.HC_FFN_DOWN_INJECT, (gguf.MODEL_TENSOR.HC_FFN_DOWN, gguf.MODEL_TENSOR.HC_FFN_INJECT), False),
|
||||
(gguf.MODEL_TENSOR.ATTN_QKV_GATE, (gguf.MODEL_TENSOR.ATTN_QKV, gguf.MODEL_TENSOR.ATTN_GATE), True),
|
||||
(gguf.MODEL_TENSOR.SSM_BETA_ALPHA_CAT, (gguf.MODEL_TENSOR.SSM_BETA, gguf.MODEL_TENSOR.SSM_ALPHA), True),
|
||||
)
|
||||
|
||||
# caches one half of a merged weight until the other half arrives, then yields [a | b]
|
||||
def _merge_shared_input(self, new_name: str, data: Tensor, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
# a pre-quantized checkpoint carries per-tensor scales, which one merged weight cannot hold
|
||||
if bid is None or self.hparams.get("quantization_config"):
|
||||
yield (new_name, data)
|
||||
return
|
||||
|
||||
layer_types = self.hparams.get("layer_types") or []
|
||||
is_linear = bid < len(layer_types) and layer_types[bid] == "linear_attention"
|
||||
|
||||
match = next(((merged, parts.index(part)) for merged, parts, linear_only in self._SHARED_INPUT_MERGES
|
||||
for part in parts
|
||||
if (is_linear or not linear_only) and self.match_model_tensor_name(new_name, part, bid)), None)
|
||||
if match is None:
|
||||
yield (new_name, data)
|
||||
return
|
||||
|
||||
merged, idx = match
|
||||
halves = self._merge_parts.setdefault((bid, merged), {})
|
||||
halves[idx] = data
|
||||
if len(halves) == 2:
|
||||
del self._merge_parts[(bid, merged)]
|
||||
yield (self.format_tensor_name(merged, bid), torch.cat([halves[0], halves[1]], dim=0))
|
||||
|
||||
# the shards concatenate into a tensor of well over 100 GB
|
||||
# use LazyChunkedTensor here, a single shard resident at a time
|
||||
@@ -214,6 +251,8 @@ class Qwen4ExpTextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase):
|
||||
super().prepare_tensors()
|
||||
if self._mtp_fc:
|
||||
raise ValueError(f"MTP projection missing its other half: {sorted(self._mtp_fc)}")
|
||||
if self._merge_parts:
|
||||
raise ValueError(f"merged weights missing a half: {sorted((bid, t.name) for bid, t in self._merge_parts)}")
|
||||
n_parts = self.hparams.get("split_ngram_parts", 0)
|
||||
if self._ple_shards and len(self._ple_shards) != n_parts:
|
||||
raise ValueError(
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
from .base import MmprojModel, ModelBase, gguf
|
||||
from .qwen import Qwen2Model
|
||||
|
||||
|
||||
@ModelBase.example("sensenova/SenseNova-U1.5-8B-MoT")
|
||||
@ModelBase.register("NEOChatModel")
|
||||
class SenseNovaU1Model(Qwen2Model):
|
||||
model_arch = gguf.MODEL_ARCH.SENSENOVA_U1
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item):
|
||||
name, _ = item
|
||||
if not name.startswith("language_model.") or "_mot_gen" in name:
|
||||
return None
|
||||
return super().filter_tensors(item)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
if int(self.hparams.get("num_experts") or 0) > 1:
|
||||
raise ValueError("SenseNova U1 conversion currently supports the dense understanding branch")
|
||||
if self.rope_parameters.get("rope_type", "default") not in ("default", None):
|
||||
raise ValueError("SenseNova U1 scaled RoPE is not supported")
|
||||
if self.hparams.get("attention_bias", False):
|
||||
raise ValueError("SenseNova U1 attention bias is not supported")
|
||||
if self.hparams.get("use_sliding_window", False):
|
||||
raise ValueError("SenseNova U1 sliding attention is not supported")
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_rope_freq_base_spatial(self.hparams["rope_theta_hw"])
|
||||
|
||||
def get_tensors(self):
|
||||
for name, gen in self.model_tensors.items():
|
||||
if name.endswith((".q_norm_hw.weight", ".k_norm_hw.weight")):
|
||||
continue
|
||||
data = gen()
|
||||
if name.endswith((".q_norm.weight", ".k_norm.weight")):
|
||||
spatial = self.model_tensors[name.replace("_norm.weight", "_norm_hw.weight")]()
|
||||
# Keep one weight vector; the graph normalizes its two halves independently.
|
||||
data = torch.cat((data, spatial), dim=0)
|
||||
yield name, data
|
||||
|
||||
|
||||
@ModelBase.register("NEOVisionModel")
|
||||
@ModelBase.example("sensenova/SenseNova-U1.5-8B-MoT")
|
||||
class SenseNovaU1VisionModel(MmprojModel):
|
||||
def __init__(self, dir_model: Path, *args, **kwargs):
|
||||
hparams = kwargs.pop("hparams", None)
|
||||
if hparams is None:
|
||||
hparams = ModelBase.load_hparams(dir_model, is_mistral_format=False)
|
||||
else:
|
||||
hparams = dict(hparams)
|
||||
|
||||
vision_config = dict(hparams["vision_config"])
|
||||
# NEOVisionModel has no transformer blocks
|
||||
# these defaults satisfy the generic mmproj metadata contract without replacing checkpoint values
|
||||
vision_config.setdefault("image_size", 0)
|
||||
vision_config.setdefault("intermediate_size", 0)
|
||||
vision_config.setdefault("num_hidden_layers", 0)
|
||||
vision_config.setdefault("num_attention_heads", 1)
|
||||
hparams["vision_config"] = vision_config
|
||||
super().__init__(dir_model, *args, hparams=hparams, **kwargs)
|
||||
|
||||
self.preprocessor_config.setdefault("image_mean", [0.485, 0.456, 0.406])
|
||||
self.preprocessor_config.setdefault("image_std", [0.229, 0.224, 0.225])
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item):
|
||||
name, gen = item
|
||||
if not name.startswith("vision_model."):
|
||||
return None
|
||||
return super().filter_tensors((name, gen))
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_clip_projector_type("sensenova_u1")
|
||||
vision_config = self.hparams["vision_config"]
|
||||
# These defaults match the known U1.5 checkpoint when older configs omit fields.
|
||||
hidden_size = int(vision_config.get("hidden_size", 1024))
|
||||
min_pixels = int(vision_config.get("min_pixels", 65536))
|
||||
max_pixels = int(vision_config.get("max_pixels", 16777216))
|
||||
downsample_ratio = float(vision_config.get("downsample_ratio", 0.5))
|
||||
if downsample_ratio <= 0:
|
||||
raise ValueError(f"SenseNova U1 downsample_ratio must be positive, got {downsample_ratio}")
|
||||
merge_size = round(1.0 / downsample_ratio)
|
||||
if abs((1.0 / downsample_ratio) - merge_size) > 1e-6:
|
||||
raise ValueError(f"SenseNova U1 downsample_ratio must have an integer reciprocal, got {downsample_ratio}")
|
||||
norm_eps = float(vision_config.get("layer_norm_eps", vision_config.get("rms_norm_eps", 1e-6)))
|
||||
self.gguf_writer.add_vision_head_dim(hidden_size)
|
||||
self.gguf_writer.add_vision_attention_layernorm_eps(norm_eps)
|
||||
self.gguf_writer.add_vision_min_pixels(min_pixels)
|
||||
self.gguf_writer.add_vision_max_pixels(max_pixels)
|
||||
self.gguf_writer.add_vision_spatial_merge_size(merge_size)
|
||||
|
||||
def modify_tensors(self, data_torch, name, bid):
|
||||
tensor_names = {
|
||||
"vision_model.embeddings.patch_embedding.weight": "v.patch_embd.weight",
|
||||
"vision_model.embeddings.patch_embedding.bias": "v.patch_embd.bias",
|
||||
"vision_model.embeddings.dense_embedding.weight": self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ, 0),
|
||||
"vision_model.embeddings.dense_embedding.bias": self.format_tensor_name(
|
||||
gguf.MODEL_TENSOR.V_MMPROJ, 0, suffix=".bias"
|
||||
),
|
||||
}
|
||||
if name not in tensor_names:
|
||||
raise ValueError(f"Unexpected SenseNova U1 vision tensor: {name!r}")
|
||||
return [(tensor_names[name], data_torch)]
|
||||
|
||||
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
||||
if new_name == self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ, 0):
|
||||
return gguf.GGMLQuantizationType.F16 if self.ftype == gguf.LlamaFileType.MOSTLY_F16 else gguf.GGMLQuantizationType.F32
|
||||
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
||||
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from .base import ModelBase, MmprojModel, gguf, logger
|
||||
from .qwen import Qwen3Model
|
||||
|
||||
|
||||
def _is_soprano_checkpoint(dir_model: Path) -> bool:
|
||||
if not (dir_model / "decoder.pth").is_file() or not (dir_model / "config.json").is_file():
|
||||
return False
|
||||
with open(dir_model / "config.json", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
if config.get("model_type") != "qwen3" or config.get("bos_token_id") != 3 or config.get("eos_token_id") != 3:
|
||||
return False
|
||||
if not (dir_model / "tokenizer.json").is_file():
|
||||
return False
|
||||
with open(dir_model / "tokenizer.json", encoding="utf-8") as f:
|
||||
added = {t["id"]: t["content"] for t in json.load(f).get("added_tokens", [])}
|
||||
return [added.get(i) for i in range(4)] == ["[UNK]", "[TEXT]", "[START]", "[STOP]"]
|
||||
|
||||
|
||||
@ModelBase.register_hparams_loader(_is_soprano_checkpoint)
|
||||
def _load_soprano_hparams(dir_model: Path) -> dict[str, Any]:
|
||||
logger.info("gguf: detected Soprano checkpoint")
|
||||
hparams = ModelBase.load_hparams(dir_model, False, guess=False)
|
||||
# the text model stays Qwen3, the mmproj holds the decoder
|
||||
hparams["text_config"] = {"architectures": ["SopranoTextModel"]}
|
||||
hparams["architectures"] = ["SopranoModel"]
|
||||
return hparams
|
||||
|
||||
|
||||
@ModelBase.register("SopranoTextModel")
|
||||
class SopranoTextModel(Qwen3Model):
|
||||
model_arch = gguf.MODEL_ARCH.QWEN3
|
||||
|
||||
def get_vocab_base_pre(self, tokenizer) -> str:
|
||||
return "soprano"
|
||||
|
||||
def set_vocab(self):
|
||||
super().set_vocab()
|
||||
self.gguf_writer.add_unk_token_id(0)
|
||||
|
||||
|
||||
@ModelBase.register("SopranoModel")
|
||||
@ModelBase.example("ekwek/Soprano-80M", "ekwek/Soprano-1.1-80M")
|
||||
class SopranoModel(MmprojModel):
|
||||
has_vision_encoder = False
|
||||
has_audio_encoder = False
|
||||
|
||||
def _load_decoder(self) -> dict[str, torch.Tensor]:
|
||||
state = torch.load(self.dir_model / "decoder.pth", map_location="cpu", mmap=True, weights_only=True)
|
||||
return state.get("state_dict", state)
|
||||
|
||||
def get_audio_config(self):
|
||||
state = self._load_decoder()
|
||||
n_embd, n_inp, _ = state["decoder.embed.weight"].shape
|
||||
n_ff = state["decoder.convnext.0.pwconv1.weight"].shape[0]
|
||||
n_layer = sum(1 for name in state if name.endswith(".pwconv1.weight"))
|
||||
if n_inp != self.n_embd_text:
|
||||
raise ValueError(f"Soprano decoder input dim {n_inp} does not match the backbone dim {self.n_embd_text}")
|
||||
return {"num_hidden_layers": n_layer, "hidden_size": n_embd, "intermediate_size": n_ff}
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
self.gguf_writer.add_file_type(self.ftype)
|
||||
self.gguf_writer.add_clip_has_gen_audio_encoder(True)
|
||||
self.gguf_writer.add_clip_gen_audio_projector_type(gguf.VisionProjectorType.SOPRANO)
|
||||
self.gguf_writer.add_gen_audio_projection_dim(self.n_embd_text)
|
||||
self.gguf_writer.add_gen_audio_embedding_length(self.hparams["hidden_size"])
|
||||
self.gguf_writer.add_gen_audio_feed_forward_length(self.hparams["intermediate_size"])
|
||||
self.gguf_writer.add_gen_audio_block_count(self.hparams["num_hidden_layers"])
|
||||
self.gguf_writer.add_gen_audio_head_count(1)
|
||||
self.gguf_writer.add_gen_audio_attention_layernorm_eps(1e-6)
|
||||
|
||||
def get_tensors(self):
|
||||
yield from self._load_decoder().items()
|
||||
|
||||
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
||||
if "dwconv.weight" in new_name or (name == "decoder.embed.weight" and n_dims == 3):
|
||||
return gguf.GGMLQuantizationType.F16
|
||||
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
||||
|
||||
def modify_tensors(self, data_torch, name, bid):
|
||||
if name == "head.istft.window":
|
||||
# the ISTFT in mtmd builds its own window
|
||||
if not torch.equal(data_torch.float(), torch.hann_window(data_torch.shape[0])):
|
||||
raise ValueError("Soprano requires a periodic Hann window")
|
||||
return
|
||||
if name == "decoder.embed.weight" and data_torch.shape[-1] == 1:
|
||||
data_torch = data_torch.squeeze(-1)
|
||||
|
||||
T = gguf.MODEL_TENSOR
|
||||
if name.startswith("decoder.convnext."):
|
||||
_, _, blk, key, *suffix = name.split(".")
|
||||
block_map = {
|
||||
"dwconv": T.A_GEN_WAV_UP_DWCONV,
|
||||
"norm": T.A_GEN_WAV_UP_NORM,
|
||||
"pwconv1": T.A_GEN_WAV_UP_PW1,
|
||||
"pwconv2": T.A_GEN_WAV_UP_PW2,
|
||||
"gamma": T.A_GEN_WAV_UP_GAMMA,
|
||||
}
|
||||
yield self.format_tensor_name(block_map[key], int(blk), suffix="".join("." + s for s in suffix)), data_torch
|
||||
return
|
||||
|
||||
tensor_map = {
|
||||
"decoder.embed": T.A_GEN_WAV_INPUT,
|
||||
"decoder.norm": T.A_GEN_WAV_NORM,
|
||||
"decoder.final_layer_norm": T.A_GEN_WAV_OUTPUT_NORM,
|
||||
"head.out": T.A_GEN_WAV_OUTPUT,
|
||||
}
|
||||
key, suffix = name.rsplit(".", 1)
|
||||
yield self.format_tensor_name(tensor_map[key], suffix="." + suffix), data_torch
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user