diff --git a/.gitattributes b/.gitattributes
index b1279845bf5..10c214441b0 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -15,8 +15,12 @@ ml/backend/**/*.cu linguist-vendored
ml/backend/**/*.cuh linguist-vendored
ml/backend/**/*.m linguist-vendored
ml/backend/**/*.metal linguist-vendored
+ml/backend/**/*.comp linguist-vendored
+ml/backend/**/*.glsl linguist-vendored
ml/backend/**/CMakeLists.txt linguist-vendored
+app/webview linguist-vendored
+
llama/build-info.cpp linguist-generated
ml/backend/ggml/ggml/src/ggml-metal/ggml-metal-embed.s linguist-generated
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index a7d0cf3b3fa..b4b9602b947 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -16,13 +16,15 @@ jobs:
outputs:
GOFLAGS: ${{ steps.goflags.outputs.GOFLAGS }}
VERSION: ${{ steps.goflags.outputs.VERSION }}
+ vendorsha: ${{ steps.changes.outputs.vendorsha }}
steps:
- uses: actions/checkout@v4
- name: Set environment
id: goflags
run: |
- echo GOFLAGS="'-ldflags=-w -s \"-X=github.com/ollama/ollama/version.Version=${GITHUB_REF_NAME#v}\" \"-X=github.com/ollama/ollama/server.mode=release\"'" >>$GITHUB_OUTPUT
- echo VERSION="${GITHUB_REF_NAME#v}" >>$GITHUB_OUTPUT
+ echo GOFLAGS="'-ldflags=-w -s \"-X=github.com/ollama/ollama/version.Version=${GITHUB_REF_NAME#v}\" \"-X=github.com/ollama/ollama/server.mode=release\"'" | tee -a $GITHUB_OUTPUT
+ echo VERSION="${GITHUB_REF_NAME#v}" | tee -a $GITHUB_OUTPUT
+ echo vendorsha=$(make -f Makefile.sync print-base) | tee -a $GITHUB_OUTPUT
darwin-build:
runs-on: macos-14-xlarge
@@ -53,6 +55,9 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
+ cache-dependency-path: |
+ go.sum
+ Makefile.sync
- run: |
./scripts/build_darwin.sh
- name: Log build results
@@ -185,7 +190,7 @@ jobs:
- uses: actions/cache@v4
with:
path: ${{ github.workspace }}\.ccache
- key: ccache-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.preset }}
+ key: ccache-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.preset }}-${{ needs.setup-environment.outputs.vendorsha }}
- name: Build target "${{ matrix.preset }}"
run: |
Import-Module 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\Microsoft.VisualStudio.DevShell.dll'
@@ -249,6 +254,9 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
+ cache-dependency-path: |
+ go.sum
+ Makefile.sync
- name: Verify gcc is actually clang
run: |
$ErrorActionPreference='Continue'
@@ -302,6 +310,9 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
+ cache-dependency-path: |
+ go.sum
+ Makefile.sync
- uses: actions/download-artifact@v4
with:
pattern: depends-windows*
@@ -366,6 +377,7 @@ jobs:
bin/ollama) echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}.tar.in ;;
lib/ollama/*.so*) echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}.tar.in ;;
lib/ollama/cuda_v*) echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}.tar.in ;;
+ lib/ollama/vulkan*) echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}.tar.in ;;
lib/ollama/cuda_jetpack5) echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}-jetpack5.tar.in ;;
lib/ollama/cuda_jetpack6) echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}-jetpack6.tar.in ;;
lib/ollama/rocm) echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}-rocm.tar.in ;;
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 82f2b0403bd..b614d2f0481 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -22,6 +22,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.changes.outputs.changed }}
+ vendorsha: ${{ steps.changes.outputs.vendorsha }}
steps:
- uses: actions/checkout@v4
with:
@@ -37,6 +38,7 @@ jobs:
}
echo changed=$(changed 'llama/llama.cpp/**/*' 'ml/backend/ggml/ggml/**/*') | tee -a $GITHUB_OUTPUT
+ echo vendorsha=$(make -f Makefile.sync print-base) | tee -a $GITHUB_OUTPUT
linux:
needs: [changes]
@@ -83,7 +85,7 @@ jobs:
- uses: actions/cache@v4
with:
path: /github/home/.cache/ccache
- key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.preset }}
+ key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.preset }}-${{ needs.changes.outputs.vendorsha }}
- run: |
cmake --preset ${{ matrix.preset }} ${{ matrix.flags }}
cmake --build --preset ${{ matrix.preset }} --parallel
@@ -178,7 +180,7 @@ jobs:
- uses: actions/cache@v4
with:
path: ${{ github.workspace }}\.ccache
- key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.preset }}
+ key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.preset }}-${{ needs.changes.outputs.vendorsha }}
- run: |
Import-Module 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\Microsoft.VisualStudio.DevShell.dll'
Enter-VsDevShell -VsInstallPath 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise' -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -no_logo'
@@ -206,6 +208,9 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
+ cache-dependency-path: |
+ go.sum
+ Makefile.sync
- uses: actions/setup-node@v4
with:
node-version: '20'
@@ -226,12 +231,9 @@ jobs:
if: always()
run: go test -count=1 -benchtime=1x ./...
- # TODO(bmizerany): replace this heavy tool with just the
- # tools/checks/binaries we want and then make them all run in parallel
- # across jobs, not on a single tiny vm on Github Actions.
- - uses: golangci/golangci-lint-action@v6
+ - uses: golangci/golangci-lint-action@v9
with:
- args: --timeout 10m0s -v
+ only-new-issues: true
patches:
runs-on: ubuntu-latest
@@ -240,4 +242,4 @@ jobs:
- name: Verify patches apply cleanly and do not change files
run: |
make -f Makefile.sync clean checkout apply-patches sync
- git diff --compact-summary --exit-code
\ No newline at end of file
+ git diff --compact-summary --exit-code
diff --git a/.golangci.yaml b/.golangci.yaml
index 9d6705bd361..5a4254132ba 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,5 +1,4 @@
-run:
- timeout: 5m
+version: "2"
linters:
enable:
- asasalint
@@ -7,35 +6,46 @@ linters:
- bodyclose
- containedctx
- gocheckcompilerdirectives
- - gofmt
- - gofumpt
- - gosimple
- - govet
- - ineffassign
- intrange
- makezero
- misspell
- nilerr
- nolintlint
- nosprintfhostport
- - staticcheck
- unconvert
- usetesting
- wastedassign
- whitespace
disable:
- - usestdlibvars
- errcheck
-linters-settings:
- staticcheck:
- checks:
- - all
- - -SA1019 # omit Deprecated check
+ - usestdlibvars
+ settings:
+ govet:
+ disable:
+ - unusedresult
+ staticcheck:
+ checks:
+ - all
+ - -QF* # disable quick fix suggestions
+ - -SA1019
+ - -ST1000 # package comment format
+ - -ST1003 # underscores in package names
+ - -ST1005 # error strings should not be capitalized
+ - -ST1012 # error var naming (ErrFoo)
+ - -ST1016 # receiver name consistency
+ - -ST1020 # comment on exported function format
+ - -ST1021 # comment on exported type format
+ - -ST1022 # comment on exported var format
+ - -ST1023 # omit type from declaration
severity:
- default-severity: error
+ default: error
rules:
- linters:
- gofmt
- goimports
- intrange
severity: info
+formatters:
+ enable:
+ - gofmt
+ - gofumpt
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4ecf19e7c97..35a0e079926 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -54,6 +54,13 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/ml/backend/ggml/ggml/src/ggml-cp
add_compile_definitions(NDEBUG GGML_VERSION=0x0 GGML_COMMIT=0x0)
+# Define GGML version variables for shared library SOVERSION
+# These are required by ggml/src/CMakeLists.txt for proper library versioning
+set(GGML_VERSION_MAJOR 0)
+set(GGML_VERSION_MINOR 0)
+set(GGML_VERSION_PATCH 0)
+set(GGML_VERSION "${GGML_VERSION_MAJOR}.${GGML_VERSION_MINOR}.${GGML_VERSION_PATCH}")
+
set(GGML_CPU ON)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ml/backend/ggml/ggml/src)
set_property(TARGET ggml PROPERTY EXCLUDE_FROM_ALL TRUE)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 10190e04458..8d51cc1de83 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,7 +16,7 @@ See the [development documentation](./docs/development.md) for instructions on h
* New features: new features (e.g. API fields, environment variables) add surface area to Ollama and make it harder to maintain in the long run as they cannot be removed without potentially breaking users in the future.
* Refactoring: large code improvements are important, but can be harder or take longer to review and merge.
-* Documentation: small updates to fill in or correct missing documentation is helpful, however large documentation additions can be hard to maintain over time.
+* Documentation: small updates to fill in or correct missing documentation are helpful, however large documentation additions can be hard to maintain over time.
### Issues that may not be accepted
@@ -43,7 +43,7 @@ Tips for proposals:
* Explain how the change will be tested.
Additionally, for bonus points: Provide draft documentation you would expect to
-see if the change were accepted.
+see if the changes were accepted.
## Pull requests
@@ -66,7 +66,6 @@ Examples:
llm/backend/mlx: support the llama architecture
CONTRIBUTING: provide clarity on good commit messages, and bad
- docs: simplify manual installation with shorter curl commands
Bad Examples:
diff --git a/Dockerfile b/Dockerfile
index 828ad434279..0084767baed 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@ ARG FLAVOR=${TARGETARCH}
ARG PARALLEL=8
ARG ROCMVERSION=6.3.3
-ARG ROCM7VERSION=7.0.2
+ARG ROCM7VERSION=7.1.1
ARG JETPACK5VERSION=r35.4.1
ARG JETPACK6VERSION=r36.4.0
ARG CMAKEVERSION=3.31.2
@@ -42,8 +42,6 @@ ENV CC=clang CXX=clang++
FROM base-${TARGETARCH} AS base
ARG CMAKEVERSION
RUN curl -fsSL https://github.com/Kitware/CMake/releases/download/v${CMAKEVERSION}/cmake-${CMAKEVERSION}-linux-$(uname -m).tar.gz | tar xz -C /usr/local --strip-components 1
-COPY CMakeLists.txt CMakePresets.json .
-COPY ml/backend/ggml/ggml ml/backend/ggml/ggml
ENV LDFLAGS=-s
FROM base AS cpu
@@ -52,6 +50,8 @@ RUN dnf install -y gcc-toolset-11-gcc gcc-toolset-11-gcc-c++ \
&& rm -rf /var/cache/dnf/* /var/lib/dnf/*.sqlite* /var/lib/dnf/history.* /tmp/*
ENV PATH=/opt/rh/gcc-toolset-11/root/usr/bin:$PATH
ARG PARALLEL
+COPY CMakeLists.txt CMakePresets.json .
+COPY ml/backend/ggml/ggml ml/backend/ggml/ggml
RUN --mount=type=cache,target=/root/.ccache \
cmake --preset 'CPU' \
&& cmake --build --parallel ${PARALLEL} --preset 'CPU' \
@@ -62,6 +62,8 @@ ARG CUDA11VERSION=11.8
RUN dnf install -y cuda-toolkit-${CUDA11VERSION//./-}
ENV PATH=/usr/local/cuda-11/bin:$PATH
ARG PARALLEL
+COPY CMakeLists.txt CMakePresets.json .
+COPY ml/backend/ggml/ggml ml/backend/ggml/ggml
RUN --mount=type=cache,target=/root/.ccache \
cmake --preset 'CUDA 11' -DOLLAMA_RUNNER_DIR="cuda_v11" \
&& cmake --build --parallel ${PARALLEL} --preset 'CUDA 11' \
@@ -72,6 +74,8 @@ ARG CUDA12VERSION=12.8
RUN dnf install -y cuda-toolkit-${CUDA12VERSION//./-}
ENV PATH=/usr/local/cuda-12/bin:$PATH
ARG PARALLEL
+COPY CMakeLists.txt CMakePresets.json .
+COPY ml/backend/ggml/ggml ml/backend/ggml/ggml
RUN --mount=type=cache,target=/root/.ccache \
cmake --preset 'CUDA 12' -DOLLAMA_RUNNER_DIR="cuda_v12"\
&& cmake --build --parallel ${PARALLEL} --preset 'CUDA 12' \
@@ -83,6 +87,8 @@ ARG CUDA13VERSION=13.0
RUN dnf install -y cuda-toolkit-${CUDA13VERSION//./-}
ENV PATH=/usr/local/cuda-13/bin:$PATH
ARG PARALLEL
+COPY CMakeLists.txt CMakePresets.json .
+COPY ml/backend/ggml/ggml ml/backend/ggml/ggml
RUN --mount=type=cache,target=/root/.ccache \
cmake --preset 'CUDA 13' -DOLLAMA_RUNNER_DIR="cuda_v13" \
&& cmake --build --parallel ${PARALLEL} --preset 'CUDA 13' \
@@ -92,6 +98,8 @@ RUN --mount=type=cache,target=/root/.ccache \
FROM base AS rocm-6
ENV PATH=/opt/rocm/hcc/bin:/opt/rocm/hip/bin:/opt/rocm/bin:/opt/rocm/hcc/bin:$PATH
ARG PARALLEL
+COPY CMakeLists.txt CMakePresets.json .
+COPY ml/backend/ggml/ggml ml/backend/ggml/ggml
RUN --mount=type=cache,target=/root/.ccache \
cmake --preset 'ROCm 6' -DOLLAMA_RUNNER_DIR="rocm" \
&& cmake --build --parallel ${PARALLEL} --preset 'ROCm 6' \
@@ -155,6 +163,8 @@ RUN --mount=type=cache,target=/root/.ccache \
&& cmake --install build --component CUDA --strip --parallel ${PARALLEL}
FROM base AS vulkan
+COPY CMakeLists.txt CMakePresets.json .
+COPY ml/backend/ggml/ggml ml/backend/ggml/ggml
RUN --mount=type=cache,target=/root/.ccache \
cmake --preset 'Vulkan' -DOLLAMA_RUNNER_DIR="vulkan" \
&& cmake --build --parallel --preset 'Vulkan' \
@@ -240,4 +250,4 @@ ENV LD_LIBRARY_PATH=/opt/rocm/lib:/usr/lib/ollama
EXPOSE 11434
ENTRYPOINT ["/usr/bin/ollama"]
-CMD ["serve"]
\ No newline at end of file
+CMD ["serve"]
diff --git a/Makefile.sync b/Makefile.sync
index b1fcde45585..c1c24f2f5dc 100644
--- a/Makefile.sync
+++ b/Makefile.sync
@@ -1,6 +1,6 @@
UPSTREAM=https://github.com/ggml-org/llama.cpp.git
WORKDIR=llama/vendor
-FETCH_HEAD=3cfa9c3f125763305b4226bc032f1954f08990dc
+FETCH_HEAD=ec98e2002
.PHONY: help
help:
@@ -57,7 +57,7 @@ checkout: $(WORKDIR)
$(WORKDIR):
git clone $(UPSTREAM) $(WORKDIR)
-.PHONE: format-patches
+.PHONY: format-patches
format-patches: llama/patches
git -C $(WORKDIR) format-patch \
--no-signature \
@@ -66,7 +66,11 @@ format-patches: llama/patches
-o $(realpath $<) \
$(FETCH_HEAD)
-.PHONE: clean
+.PHONY: clean
clean: checkout
@git -C $(WORKDIR) am --abort || true
$(RM) llama/patches/.*.patched
+
+.PHONY: print-base
+print-base:
+ @echo $(FETCH_HEAD)
\ No newline at end of file
diff --git a/README.md b/README.md
index 422f2b12a73..bb08819d67c 100644
--- a/README.md
+++ b/README.md
@@ -367,6 +367,7 @@ See the [API documentation](./docs/api.md) for all endpoints.
- [Ollama4j Web UI](https://github.com/ollama4j/ollama4j-web-ui) - Java-based Web UI for Ollama built with Vaadin, Spring Boot, and Ollama4j
- [PyOllaMx](https://github.com/kspviswa/pyOllaMx) - macOS application capable of chatting with both Ollama and Apple MLX models.
- [Cline](https://github.com/cline/cline) - Formerly known as Claude Dev is a VS Code extension for multi-file/whole-repo coding
+- [Void](https://github.com/voideditor/void) (Open source AI code editor and Cursor alternative)
- [Cherry Studio](https://github.com/kangfenmao/cherry-studio) (Desktop client with Ollama support)
- [ConfiChat](https://github.com/1runeberg/confichat) (Lightweight, standalone, multi-platform, and privacy-focused LLM chat interface with optional encryption)
- [Archyve](https://github.com/nickthecook/archyve) (RAG-enabling document library)
@@ -427,6 +428,7 @@ See the [API documentation](./docs/api.md) for all endpoints.
- [Mayan EDMS](https://gitlab.com/mayan-edms/mayan-edms) (Open source document management system to organize, tag, search, and automate your files with powerful Ollama driven workflows.)
- [Serene Pub](https://github.com/doolijb/serene-pub) (Beginner friendly, open source AI Roleplaying App for Windows, Mac OS and Linux. Search, download and use models with Ollama all inside the app.)
- [Andes](https://github.com/aqerd/andes) (A Visual Studio Code extension that provides a local UI interface for Ollama models)
+- [KDeps](https://github.com/kdeps/kdeps) (Kdeps is an offline-first AI framework for building Dockerized full-stack AI applications declaratively using Apple PKL and integrates APIs with Ollama on the backend.)
- [Clueless](https://github.com/KashyapTan/clueless) (Open Source & Local Cluely: A desktop application LLM assistant to help you talk to anything on your screen using locally served Ollama models. Also undetectable to screenshare)
- [ollama-co2](https://github.com/carbonatedWaterOrg/ollama-co2) (FastAPI web interface for monitoring and managing local and remote Ollama servers with real-time model monitoring and concurrent downloads)
- [Hillnote](https://hillnote.com) (A Markdown-first workspace designed to supercharge your AI workflow. Create documents ready to integrate with Claude, ChatGPT, Gemini, Cursor, and more - all while keeping your work on your device.)
@@ -553,7 +555,7 @@ See the [API documentation](./docs/api.md) for all endpoints.
- [Parakeet](https://github.com/parakeet-nest/parakeet) is a GoLang library, made to simplify the development of small generative AI applications with Ollama.
- [Haverscript](https://github.com/andygill/haverscript) with [examples](https://github.com/andygill/haverscript/tree/main/examples)
- [Ollama for Swift](https://github.com/mattt/ollama-swift)
-- [Swollama for Swift](https://github.com/marcusziade/Swollama) with [DocC](https://marcusziade.github.io/Swollama/documentation/swollama/)
+- [Swollama for Swift](https://github.com/guitaripod/Swollama) with [DocC](https://guitaripod.github.io/Swollama/documentation/swollama)
- [GoLamify](https://github.com/prasad89/golamify)
- [Ollama for Haskell](https://github.com/tusharad/ollama-haskell)
- [multi-llm-ts](https://github.com/nbonamy/multi-llm-ts) (A Typescript/JavaScript library allowing access to different LLM in a unified API)
@@ -616,7 +618,7 @@ See the [API documentation](./docs/api.md) for all endpoints.
- [LSP-AI](https://github.com/SilasMarvin/lsp-ai) (Open-source language server for AI-powered functionality)
- [QodeAssist](https://github.com/Palm1r/QodeAssist) (AI-powered coding assistant plugin for Qt Creator)
- [Obsidian Quiz Generator plugin](https://github.com/ECuiDev/obsidian-quiz-generator)
-- [AI Summmary Helper plugin](https://github.com/philffm/ai-summary-helper)
+- [AI Summary Helper plugin](https://github.com/philffm/ai-summary-helper)
- [TextCraft](https://github.com/suncloudsmoon/TextCraft) (Copilot in Word alternative using Ollama)
- [Alfred Ollama](https://github.com/zeitlings/alfred-ollama) (Alfred Workflow)
- [TextLLaMA](https://github.com/adarshM84/TextLLaMA) A Chrome Extension that helps you write emails, correct grammar, and translate into any language
@@ -624,7 +626,7 @@ See the [API documentation](./docs/api.md) for all endpoints.
- [LLM Telegram Bot](https://github.com/innightwolfsleep/llm_telegram_bot) (telegram bot, primary for RP. Oobabooga-like buttons, [A1111](https://github.com/AUTOMATIC1111/stable-diffusion-webui) API integration e.t.c)
- [mcp-llm](https://github.com/sammcj/mcp-llm) (MCP Server to allow LLMs to call other LLMs)
- [SimpleOllamaUnity](https://github.com/HardCodeDev777/SimpleOllamaUnity) (Unity Engine extension for communicating with Ollama in a few lines of code. Also works at runtime)
-- [UnityCodeLama](https://github.com/HardCodeDev777/UnityCodeLama) (Unity Edtior tool to analyze scripts via Ollama)
+- [UnityCodeLama](https://github.com/HardCodeDev777/UnityCodeLama) (Unity Editor tool to analyze scripts via Ollama)
- [NativeMind](https://github.com/NativeMindBrowser/NativeMindExtension) (Private, on-device AI Assistant, no cloud dependencies)
- [GMAI - Gradle Managed AI](https://gmai.premex.se/) (Gradle plugin for automated Ollama lifecycle management during build phases)
- [NOMYO Router](https://github.com/nomyo-ai/nomyo-router) (A transparent Ollama proxy with model deployment aware routing which auto-manages multiple Ollama instances in a given network)
@@ -634,7 +636,7 @@ See the [API documentation](./docs/api.md) for all endpoints.
- [llama.cpp](https://github.com/ggml-org/llama.cpp) project founded by Georgi Gerganov.
### Observability
-- [Opik](https://www.comet.com/docs/opik/cookbook/ollama) is an open-source platform to debug, evaluate, and monitor your LLM applications, RAG systems, and agentic workflows with comprehensive tracing, automated evaluations, and production-ready dashboards. Opik supports native intergration to Ollama.
+- [Opik](https://www.comet.com/docs/opik/cookbook/ollama) is an open-source platform to debug, evaluate, and monitor your LLM applications, RAG systems, and agentic workflows with comprehensive tracing, automated evaluations, and production-ready dashboards. Opik supports native integration to Ollama.
- [Lunary](https://lunary.ai/docs/integrations/ollama) is the leading open-source LLM observability platform. It provides a variety of enterprise-grade features such as real-time analytics, prompt templates management, PII masking, and comprehensive agent tracing.
- [OpenLIT](https://github.com/openlit/openlit) is an OpenTelemetry-native tool for monitoring Ollama Applications & GPUs using traces and metrics.
- [HoneyHive](https://docs.honeyhive.ai/integrations/ollama) is an AI observability and evaluation platform for AI agents. Use HoneyHive to evaluate agent performance, interrogate failures, and monitor quality in production.
diff --git a/SECURITY.md b/SECURITY.md
index d38bb7c4e24..43fe3102d4a 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -14,7 +14,7 @@ Please include the following details in your report:
## Security best practices
-While the maintainer team does their best to secure Ollama, users are encouraged to implement their own security best practices, such as:
+While the maintainer team does its best to secure Ollama, users are encouraged to implement their own security best practices, such as:
- Regularly updating to the latest version of Ollama
- Securing access to hosted instances of Ollama
diff --git a/api/client.go b/api/client.go
index 0d4c97ba9fb..c7051689902 100644
--- a/api/client.go
+++ b/api/client.go
@@ -226,7 +226,14 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f
bts := scanner.Bytes()
if err := json.Unmarshal(bts, &errorResponse); err != nil {
- return fmt.Errorf("unmarshal: %w", err)
+ if response.StatusCode >= http.StatusBadRequest {
+ return StatusError{
+ StatusCode: response.StatusCode,
+ Status: response.Status,
+ ErrorMessage: string(bts),
+ }
+ }
+ return errors.New(string(bts))
}
if response.StatusCode == http.StatusUnauthorized {
@@ -340,7 +347,7 @@ type CreateProgressFunc func(ProgressResponse) error
// Create creates a model from a [Modelfile]. fn is a progress function that
// behaves similarly to other methods (see [Client.Pull]).
//
-// [Modelfile]: https://github.com/ollama/ollama/blob/main/docs/modelfile.md
+// [Modelfile]: https://github.com/ollama/ollama/blob/main/docs/modelfile.mdx
func (c *Client) Create(ctx context.Context, req *CreateRequest, fn CreateProgressFunc) error {
return c.stream(ctx, http.MethodPost, "/api/create", req, func(bts []byte) error {
var resp ProgressResponse
diff --git a/api/client_test.go b/api/client_test.go
index f0034e02d32..827e41d9a34 100644
--- a/api/client_test.go
+++ b/api/client_test.go
@@ -55,6 +55,7 @@ func TestClientFromEnvironment(t *testing.T) {
type testError struct {
message string
statusCode int
+ raw bool // if true, write message as-is instead of JSON encoding
}
func (e testError) Error() string {
@@ -111,6 +112,20 @@ func TestClientStream(t *testing.T) {
},
},
},
+ {
+ name: "plain text error response",
+ responses: []any{
+ "internal server error",
+ },
+ wantErr: "internal server error",
+ },
+ {
+ name: "HTML error page",
+ responses: []any{
+ "
404 Not Found",
+ },
+ wantErr: "404 Not Found",
+ },
}
for _, tc := range testCases {
@@ -135,6 +150,12 @@ func TestClientStream(t *testing.T) {
return
}
+ if str, ok := resp.(string); ok {
+ fmt.Fprintln(w, str)
+ flusher.Flush()
+ continue
+ }
+
if err := json.NewEncoder(w).Encode(resp); err != nil {
t.Fatalf("failed to encode response: %v", err)
}
@@ -173,9 +194,10 @@ func TestClientStream(t *testing.T) {
func TestClientDo(t *testing.T) {
testCases := []struct {
- name string
- response any
- wantErr string
+ name string
+ response any
+ wantErr string
+ wantStatusCode int
}{
{
name: "immediate error response",
@@ -183,7 +205,8 @@ func TestClientDo(t *testing.T) {
message: "test error message",
statusCode: http.StatusBadRequest,
},
- wantErr: "test error message",
+ wantErr: "test error message",
+ wantStatusCode: http.StatusBadRequest,
},
{
name: "server error response",
@@ -191,7 +214,8 @@ func TestClientDo(t *testing.T) {
message: "internal error",
statusCode: http.StatusInternalServerError,
},
- wantErr: "internal error",
+ wantErr: "internal error",
+ wantStatusCode: http.StatusInternalServerError,
},
{
name: "successful response",
@@ -203,6 +227,26 @@ func TestClientDo(t *testing.T) {
Success: true,
},
},
+ {
+ name: "plain text error response",
+ response: testError{
+ message: "internal server error",
+ statusCode: http.StatusInternalServerError,
+ raw: true,
+ },
+ wantErr: "internal server error",
+ wantStatusCode: http.StatusInternalServerError,
+ },
+ {
+ name: "HTML error page",
+ response: testError{
+ message: "404 Not Found",
+ statusCode: http.StatusNotFound,
+ raw: true,
+ },
+ wantErr: "404 Not Found",
+ wantStatusCode: http.StatusNotFound,
+ },
}
for _, tc := range testCases {
@@ -210,11 +254,16 @@ func TestClientDo(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if errResp, ok := tc.response.(testError); ok {
w.WriteHeader(errResp.statusCode)
- err := json.NewEncoder(w).Encode(map[string]string{
- "error": errResp.message,
- })
- if err != nil {
- t.Fatal("failed to encode error response:", err)
+ if !errResp.raw {
+ err := json.NewEncoder(w).Encode(map[string]string{
+ "error": errResp.message,
+ })
+ if err != nil {
+ t.Fatal("failed to encode error response:", err)
+ }
+ } else {
+ // Write raw message (simulates non-JSON error responses)
+ fmt.Fprint(w, errResp.message)
}
return
}
@@ -241,6 +290,15 @@ func TestClientDo(t *testing.T) {
if err.Error() != tc.wantErr {
t.Errorf("error message mismatch: got %q, want %q", err.Error(), tc.wantErr)
}
+ if tc.wantStatusCode != 0 {
+ if statusErr, ok := err.(StatusError); ok {
+ if statusErr.StatusCode != tc.wantStatusCode {
+ t.Errorf("status code mismatch: got %d, want %d", statusErr.StatusCode, tc.wantStatusCode)
+ }
+ } else {
+ t.Errorf("expected StatusError, got %T", err)
+ }
+ }
return
}
diff --git a/api/examples/chat/main.go b/api/examples/chat/main.go
index 07430305774..b44a1ec9249 100644
--- a/api/examples/chat/main.go
+++ b/api/examples/chat/main.go
@@ -15,19 +15,19 @@ func main() {
}
messages := []api.Message{
- api.Message{
+ {
Role: "system",
Content: "Provide very brief, concise responses",
},
- api.Message{
+ {
Role: "user",
Content: "Name some unusual animals",
},
- api.Message{
+ {
Role: "assistant",
Content: "Monotreme, platypus, echidna",
},
- api.Message{
+ {
Role: "user",
Content: "which of these is the most dangerous?",
},
diff --git a/api/types.go b/api/types.go
index 05d63c42beb..46fee205da8 100644
--- a/api/types.go
+++ b/api/types.go
@@ -3,6 +3,7 @@ package api
import (
"encoding/json"
"fmt"
+ "iter"
"log/slog"
"math"
"os"
@@ -14,6 +15,7 @@ import (
"github.com/google/uuid"
"github.com/ollama/ollama/envconfig"
+ "github.com/ollama/ollama/internal/orderedmap"
"github.com/ollama/ollama/types/model"
)
@@ -227,13 +229,79 @@ type ToolCallFunction struct {
Arguments ToolCallFunctionArguments `json:"arguments"`
}
-type ToolCallFunctionArguments map[string]any
+// ToolCallFunctionArguments holds tool call arguments in insertion order.
+type ToolCallFunctionArguments struct {
+ om *orderedmap.Map[string, any]
+}
+
+// NewToolCallFunctionArguments creates a new empty ToolCallFunctionArguments.
+func NewToolCallFunctionArguments() ToolCallFunctionArguments {
+ return ToolCallFunctionArguments{om: orderedmap.New[string, any]()}
+}
+
+// Get retrieves a value by key.
+func (t *ToolCallFunctionArguments) Get(key string) (any, bool) {
+ if t == nil || t.om == nil {
+ return nil, false
+ }
+ return t.om.Get(key)
+}
+
+// Set sets a key-value pair, preserving insertion order.
+func (t *ToolCallFunctionArguments) Set(key string, value any) {
+ if t == nil {
+ return
+ }
+ if t.om == nil {
+ t.om = orderedmap.New[string, any]()
+ }
+ t.om.Set(key, value)
+}
+
+// Len returns the number of arguments.
+func (t *ToolCallFunctionArguments) Len() int {
+ if t == nil || t.om == nil {
+ return 0
+ }
+ return t.om.Len()
+}
+
+// All returns an iterator over all key-value pairs in insertion order.
+func (t *ToolCallFunctionArguments) All() iter.Seq2[string, any] {
+ if t == nil || t.om == nil {
+ return func(yield func(string, any) bool) {}
+ }
+ return t.om.All()
+}
+
+// ToMap returns a regular map (order not preserved).
+func (t *ToolCallFunctionArguments) ToMap() map[string]any {
+ if t == nil || t.om == nil {
+ return nil
+ }
+ return t.om.ToMap()
+}
func (t *ToolCallFunctionArguments) String() string {
- bts, _ := json.Marshal(t)
+ if t == nil || t.om == nil {
+ return "{}"
+ }
+ bts, _ := json.Marshal(t.om)
return string(bts)
}
+func (t *ToolCallFunctionArguments) UnmarshalJSON(data []byte) error {
+ t.om = orderedmap.New[string, any]()
+ return json.Unmarshal(data, t.om)
+}
+
+func (t ToolCallFunctionArguments) MarshalJSON() ([]byte, error) {
+ if t.om == nil {
+ return []byte("{}"), nil
+ }
+ return json.Marshal(t.om)
+}
+
type Tool struct {
Type string `json:"type"`
Items any `json:"items,omitempty"`
@@ -282,12 +350,78 @@ func (pt PropertyType) String() string {
return fmt.Sprintf("%v", []string(pt))
}
+// ToolPropertiesMap holds tool properties in insertion order.
+type ToolPropertiesMap struct {
+ om *orderedmap.Map[string, ToolProperty]
+}
+
+// NewToolPropertiesMap creates a new empty ToolPropertiesMap.
+func NewToolPropertiesMap() *ToolPropertiesMap {
+ return &ToolPropertiesMap{om: orderedmap.New[string, ToolProperty]()}
+}
+
+// Get retrieves a property by name.
+func (t *ToolPropertiesMap) Get(key string) (ToolProperty, bool) {
+ if t == nil || t.om == nil {
+ return ToolProperty{}, false
+ }
+ return t.om.Get(key)
+}
+
+// Set sets a property, preserving insertion order.
+func (t *ToolPropertiesMap) Set(key string, value ToolProperty) {
+ if t == nil {
+ return
+ }
+ if t.om == nil {
+ t.om = orderedmap.New[string, ToolProperty]()
+ }
+ t.om.Set(key, value)
+}
+
+// Len returns the number of properties.
+func (t *ToolPropertiesMap) Len() int {
+ if t == nil || t.om == nil {
+ return 0
+ }
+ return t.om.Len()
+}
+
+// All returns an iterator over all properties in insertion order.
+func (t *ToolPropertiesMap) All() iter.Seq2[string, ToolProperty] {
+ if t == nil || t.om == nil {
+ return func(yield func(string, ToolProperty) bool) {}
+ }
+ return t.om.All()
+}
+
+// ToMap returns a regular map (order not preserved).
+func (t *ToolPropertiesMap) ToMap() map[string]ToolProperty {
+ if t == nil || t.om == nil {
+ return nil
+ }
+ return t.om.ToMap()
+}
+
+func (t ToolPropertiesMap) MarshalJSON() ([]byte, error) {
+ if t.om == nil {
+ return []byte("null"), nil
+ }
+ return json.Marshal(t.om)
+}
+
+func (t *ToolPropertiesMap) UnmarshalJSON(data []byte) error {
+ t.om = orderedmap.New[string, ToolProperty]()
+ return json.Unmarshal(data, t.om)
+}
+
type ToolProperty struct {
- AnyOf []ToolProperty `json:"anyOf,omitempty"`
- Type PropertyType `json:"type,omitempty"`
- Items any `json:"items,omitempty"`
- Description string `json:"description,omitempty"`
- Enum []any `json:"enum,omitempty"`
+ AnyOf []ToolProperty `json:"anyOf,omitempty"`
+ Type PropertyType `json:"type,omitempty"`
+ Items any `json:"items,omitempty"`
+ Description string `json:"description,omitempty"`
+ Enum []any `json:"enum,omitempty"`
+ Properties *ToolPropertiesMap `json:"properties,omitempty"`
}
// ToTypeScriptType converts a ToolProperty to a TypeScript type string
@@ -336,11 +470,11 @@ func mapToTypeScriptType(jsonType string) string {
}
type ToolFunctionParameters struct {
- Type string `json:"type"`
- Defs any `json:"$defs,omitempty"`
- Items any `json:"items,omitempty"`
- Required []string `json:"required,omitempty"`
- Properties map[string]ToolProperty `json:"properties"`
+ Type string `json:"type"`
+ Defs any `json:"$defs,omitempty"`
+ Items any `json:"items,omitempty"`
+ Required []string `json:"required,omitempty"`
+ Properties *ToolPropertiesMap `json:"properties"`
}
func (t *ToolFunctionParameters) String() string {
@@ -555,6 +689,9 @@ type CreateRequest struct {
Renderer string `json:"renderer,omitempty"`
Parser string `json:"parser,omitempty"`
+ // Requires is the minimum version of Ollama required by the model.
+ Requires string `json:"requires,omitempty"`
+
// Info is a map of additional information for the model
Info map[string]any `json:"info,omitempty"`
@@ -605,6 +742,7 @@ type ShowResponse struct {
Tensors []Tensor `json:"tensors,omitempty"`
Capabilities []model.Capability `json:"capabilities,omitempty"`
ModifiedAt time.Time `json:"modified_at,omitempty"`
+ Requires string `json:"requires,omitempty"`
}
// CopyRequest is the request passed to [Client.Copy].
diff --git a/api/types_test.go b/api/types_test.go
index 187012db256..69d9c5a3d90 100644
--- a/api/types_test.go
+++ b/api/types_test.go
@@ -11,6 +11,24 @@ import (
"github.com/stretchr/testify/require"
)
+// testPropsMap creates a ToolPropertiesMap from a map (convenience function for tests, order not preserved)
+func testPropsMap(m map[string]ToolProperty) *ToolPropertiesMap {
+ props := NewToolPropertiesMap()
+ for k, v := range m {
+ props.Set(k, v)
+ }
+ return props
+}
+
+// testArgs creates ToolCallFunctionArguments from a map (convenience function for tests, order not preserved)
+func testArgs(m map[string]any) ToolCallFunctionArguments {
+ args := NewToolCallFunctionArguments()
+ for k, v := range m {
+ args.Set(k, v)
+ }
+ return args
+}
+
func TestKeepAliveParsingFromJSON(t *testing.T) {
tests := []struct {
name string
@@ -309,9 +327,9 @@ func TestToolFunctionParameters_MarshalJSON(t *testing.T) {
input: ToolFunctionParameters{
Type: "object",
Required: []string{"name"},
- Properties: map[string]ToolProperty{
+ Properties: testPropsMap(map[string]ToolProperty{
"name": {Type: PropertyType{"string"}},
- },
+ }),
},
expected: `{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}`,
},
@@ -319,9 +337,9 @@ func TestToolFunctionParameters_MarshalJSON(t *testing.T) {
name: "no required",
input: ToolFunctionParameters{
Type: "object",
- Properties: map[string]ToolProperty{
+ Properties: testPropsMap(map[string]ToolProperty{
"name": {Type: PropertyType{"string"}},
- },
+ }),
},
expected: `{"type":"object","properties":{"name":{"type":"string"}}}`,
},
@@ -339,7 +357,7 @@ func TestToolFunctionParameters_MarshalJSON(t *testing.T) {
func TestToolCallFunction_IndexAlwaysMarshals(t *testing.T) {
fn := ToolCallFunction{
Name: "echo",
- Arguments: ToolCallFunctionArguments{"message": "hi"},
+ Arguments: testArgs(map[string]any{"message": "hi"}),
}
data, err := json.Marshal(fn)
@@ -504,6 +522,116 @@ func TestThinking_UnmarshalJSON(t *testing.T) {
}
}
+func TestToolPropertyNestedProperties(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected ToolProperty
+ }{
+ {
+ name: "nested object properties",
+ input: `{
+ "type": "object",
+ "description": "Location details",
+ "properties": {
+ "address": {
+ "type": "string",
+ "description": "Street address"
+ },
+ "city": {
+ "type": "string",
+ "description": "City name"
+ }
+ }
+ }`,
+ expected: ToolProperty{
+ Type: PropertyType{"object"},
+ Description: "Location details",
+ Properties: testPropsMap(map[string]ToolProperty{
+ "address": {
+ Type: PropertyType{"string"},
+ Description: "Street address",
+ },
+ "city": {
+ Type: PropertyType{"string"},
+ Description: "City name",
+ },
+ }),
+ },
+ },
+ {
+ name: "deeply nested properties",
+ input: `{
+ "type": "object",
+ "description": "Event",
+ "properties": {
+ "location": {
+ "type": "object",
+ "description": "Location",
+ "properties": {
+ "coordinates": {
+ "type": "object",
+ "description": "GPS coordinates",
+ "properties": {
+ "lat": {"type": "number", "description": "Latitude"},
+ "lng": {"type": "number", "description": "Longitude"}
+ }
+ }
+ }
+ }
+ }
+ }`,
+ expected: ToolProperty{
+ Type: PropertyType{"object"},
+ Description: "Event",
+ Properties: testPropsMap(map[string]ToolProperty{
+ "location": {
+ Type: PropertyType{"object"},
+ Description: "Location",
+ Properties: testPropsMap(map[string]ToolProperty{
+ "coordinates": {
+ Type: PropertyType{"object"},
+ Description: "GPS coordinates",
+ Properties: testPropsMap(map[string]ToolProperty{
+ "lat": {Type: PropertyType{"number"}, Description: "Latitude"},
+ "lng": {Type: PropertyType{"number"}, Description: "Longitude"},
+ }),
+ },
+ }),
+ },
+ }),
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var prop ToolProperty
+ err := json.Unmarshal([]byte(tt.input), &prop)
+ require.NoError(t, err)
+
+ // Compare JSON representations since pointer comparison doesn't work
+ expectedJSON, err := json.Marshal(tt.expected)
+ require.NoError(t, err)
+ actualJSON, err := json.Marshal(prop)
+ require.NoError(t, err)
+ assert.JSONEq(t, string(expectedJSON), string(actualJSON))
+
+ // Round-trip test: marshal and unmarshal again
+ data, err := json.Marshal(prop)
+ require.NoError(t, err)
+
+ var prop2 ToolProperty
+ err = json.Unmarshal(data, &prop2)
+ require.NoError(t, err)
+
+ prop2JSON, err := json.Marshal(prop2)
+ require.NoError(t, err)
+ assert.JSONEq(t, string(expectedJSON), string(prop2JSON))
+ })
+ }
+}
+
func TestToolFunctionParameters_String(t *testing.T) {
tests := []struct {
name string
@@ -515,12 +643,12 @@ func TestToolFunctionParameters_String(t *testing.T) {
params: ToolFunctionParameters{
Type: "object",
Required: []string{"name"},
- Properties: map[string]ToolProperty{
+ Properties: testPropsMap(map[string]ToolProperty{
"name": {
Type: PropertyType{"string"},
Description: "The name of the person",
},
- },
+ }),
},
expected: `{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"The name of the person"}}}`,
},
@@ -537,7 +665,7 @@ func TestToolFunctionParameters_String(t *testing.T) {
s.Self = s
return s
}(),
- Properties: map[string]ToolProperty{},
+ Properties: testPropsMap(map[string]ToolProperty{}),
},
expected: "",
},
@@ -550,3 +678,235 @@ func TestToolFunctionParameters_String(t *testing.T) {
})
}
}
+
+func TestToolCallFunctionArguments_OrderPreservation(t *testing.T) {
+ t.Run("marshal preserves insertion order", func(t *testing.T) {
+ args := NewToolCallFunctionArguments()
+ args.Set("zebra", "z")
+ args.Set("apple", "a")
+ args.Set("mango", "m")
+
+ data, err := json.Marshal(args)
+ require.NoError(t, err)
+
+ // Should preserve insertion order, not alphabetical
+ assert.Equal(t, `{"zebra":"z","apple":"a","mango":"m"}`, string(data))
+ })
+
+ t.Run("unmarshal preserves JSON order", func(t *testing.T) {
+ jsonData := `{"zebra":"z","apple":"a","mango":"m"}`
+
+ var args ToolCallFunctionArguments
+ err := json.Unmarshal([]byte(jsonData), &args)
+ require.NoError(t, err)
+
+ // Verify iteration order matches JSON order
+ var keys []string
+ for k := range args.All() {
+ keys = append(keys, k)
+ }
+ assert.Equal(t, []string{"zebra", "apple", "mango"}, keys)
+ })
+
+ t.Run("round trip preserves order", func(t *testing.T) {
+ original := `{"z":1,"a":2,"m":3,"b":4}`
+
+ var args ToolCallFunctionArguments
+ err := json.Unmarshal([]byte(original), &args)
+ require.NoError(t, err)
+
+ data, err := json.Marshal(args)
+ require.NoError(t, err)
+
+ assert.Equal(t, original, string(data))
+ })
+
+ t.Run("String method returns ordered JSON", func(t *testing.T) {
+ args := NewToolCallFunctionArguments()
+ args.Set("c", 3)
+ args.Set("a", 1)
+ args.Set("b", 2)
+
+ assert.Equal(t, `{"c":3,"a":1,"b":2}`, args.String())
+ })
+
+ t.Run("Get retrieves correct values", func(t *testing.T) {
+ args := NewToolCallFunctionArguments()
+ args.Set("key1", "value1")
+ args.Set("key2", 42)
+
+ v, ok := args.Get("key1")
+ assert.True(t, ok)
+ assert.Equal(t, "value1", v)
+
+ v, ok = args.Get("key2")
+ assert.True(t, ok)
+ assert.Equal(t, 42, v)
+
+ _, ok = args.Get("nonexistent")
+ assert.False(t, ok)
+ })
+
+ t.Run("Len returns correct count", func(t *testing.T) {
+ args := NewToolCallFunctionArguments()
+ assert.Equal(t, 0, args.Len())
+
+ args.Set("a", 1)
+ assert.Equal(t, 1, args.Len())
+
+ args.Set("b", 2)
+ assert.Equal(t, 2, args.Len())
+ })
+
+ t.Run("empty args marshal to empty object", func(t *testing.T) {
+ args := NewToolCallFunctionArguments()
+ data, err := json.Marshal(args)
+ require.NoError(t, err)
+ assert.Equal(t, `{}`, string(data))
+ })
+
+ t.Run("zero value args marshal to empty object", func(t *testing.T) {
+ var args ToolCallFunctionArguments
+ assert.Equal(t, "{}", args.String())
+ })
+}
+
+func TestToolPropertiesMap_OrderPreservation(t *testing.T) {
+ t.Run("marshal preserves insertion order", func(t *testing.T) {
+ props := NewToolPropertiesMap()
+ props.Set("zebra", ToolProperty{Type: PropertyType{"string"}})
+ props.Set("apple", ToolProperty{Type: PropertyType{"number"}})
+ props.Set("mango", ToolProperty{Type: PropertyType{"boolean"}})
+
+ data, err := json.Marshal(props)
+ require.NoError(t, err)
+
+ // Should preserve insertion order, not alphabetical
+ expected := `{"zebra":{"type":"string"},"apple":{"type":"number"},"mango":{"type":"boolean"}}`
+ assert.Equal(t, expected, string(data))
+ })
+
+ t.Run("unmarshal preserves JSON order", func(t *testing.T) {
+ jsonData := `{"zebra":{"type":"string"},"apple":{"type":"number"},"mango":{"type":"boolean"}}`
+
+ var props ToolPropertiesMap
+ err := json.Unmarshal([]byte(jsonData), &props)
+ require.NoError(t, err)
+
+ // Verify iteration order matches JSON order
+ var keys []string
+ for k := range props.All() {
+ keys = append(keys, k)
+ }
+ assert.Equal(t, []string{"zebra", "apple", "mango"}, keys)
+ })
+
+ t.Run("round trip preserves order", func(t *testing.T) {
+ original := `{"z":{"type":"string"},"a":{"type":"number"},"m":{"type":"boolean"}}`
+
+ var props ToolPropertiesMap
+ err := json.Unmarshal([]byte(original), &props)
+ require.NoError(t, err)
+
+ data, err := json.Marshal(props)
+ require.NoError(t, err)
+
+ assert.Equal(t, original, string(data))
+ })
+
+ t.Run("Get retrieves correct values", func(t *testing.T) {
+ props := NewToolPropertiesMap()
+ props.Set("name", ToolProperty{Type: PropertyType{"string"}, Description: "The name"})
+ props.Set("age", ToolProperty{Type: PropertyType{"integer"}, Description: "The age"})
+
+ v, ok := props.Get("name")
+ assert.True(t, ok)
+ assert.Equal(t, "The name", v.Description)
+
+ v, ok = props.Get("age")
+ assert.True(t, ok)
+ assert.Equal(t, "The age", v.Description)
+
+ _, ok = props.Get("nonexistent")
+ assert.False(t, ok)
+ })
+
+ t.Run("Len returns correct count", func(t *testing.T) {
+ props := NewToolPropertiesMap()
+ assert.Equal(t, 0, props.Len())
+
+ props.Set("a", ToolProperty{})
+ assert.Equal(t, 1, props.Len())
+
+ props.Set("b", ToolProperty{})
+ assert.Equal(t, 2, props.Len())
+ })
+
+ t.Run("nil props marshal to null", func(t *testing.T) {
+ var props *ToolPropertiesMap
+ data, err := json.Marshal(props)
+ require.NoError(t, err)
+ assert.Equal(t, `null`, string(data))
+ })
+
+ t.Run("ToMap returns regular map", func(t *testing.T) {
+ props := NewToolPropertiesMap()
+ props.Set("a", ToolProperty{Type: PropertyType{"string"}})
+ props.Set("b", ToolProperty{Type: PropertyType{"number"}})
+
+ m := props.ToMap()
+ assert.Equal(t, 2, len(m))
+ assert.Equal(t, PropertyType{"string"}, m["a"].Type)
+ assert.Equal(t, PropertyType{"number"}, m["b"].Type)
+ })
+}
+
+func TestToolCallFunctionArguments_ComplexValues(t *testing.T) {
+ t.Run("nested objects preserve order", func(t *testing.T) {
+ jsonData := `{"outer":{"z":1,"a":2},"simple":"value"}`
+
+ var args ToolCallFunctionArguments
+ err := json.Unmarshal([]byte(jsonData), &args)
+ require.NoError(t, err)
+
+ // Outer keys should be in order
+ var keys []string
+ for k := range args.All() {
+ keys = append(keys, k)
+ }
+ assert.Equal(t, []string{"outer", "simple"}, keys)
+ })
+
+ t.Run("arrays as values", func(t *testing.T) {
+ args := NewToolCallFunctionArguments()
+ args.Set("items", []string{"a", "b", "c"})
+ args.Set("numbers", []int{1, 2, 3})
+
+ data, err := json.Marshal(args)
+ require.NoError(t, err)
+
+ assert.Equal(t, `{"items":["a","b","c"],"numbers":[1,2,3]}`, string(data))
+ })
+}
+
+func TestToolPropertiesMap_NestedProperties(t *testing.T) {
+ t.Run("nested properties preserve order", func(t *testing.T) {
+ props := NewToolPropertiesMap()
+
+ nestedProps := NewToolPropertiesMap()
+ nestedProps.Set("z_field", ToolProperty{Type: PropertyType{"string"}})
+ nestedProps.Set("a_field", ToolProperty{Type: PropertyType{"number"}})
+
+ props.Set("outer", ToolProperty{
+ Type: PropertyType{"object"},
+ Properties: nestedProps,
+ })
+
+ data, err := json.Marshal(props)
+ require.NoError(t, err)
+
+ // Both outer and inner should preserve order
+ expected := `{"outer":{"type":"object","properties":{"z_field":{"type":"string"},"a_field":{"type":"number"}}}}`
+ assert.Equal(t, expected, string(data))
+ })
+}
diff --git a/app/cmd/app/app.go b/app/cmd/app/app.go
index 1a4cd568f51..7e183b8df0d 100644
--- a/app/cmd/app/app.go
+++ b/app/cmd/app/app.go
@@ -273,10 +273,6 @@ func main() {
Handler: uiServer.Handler(),
}
- if _, err := uiServer.UserData(ctx); err != nil {
- slog.Warn("failed to load user data", "error", err)
- }
-
// Start the UI server
slog.Info("starting ui server", "port", port)
go func() {
@@ -320,6 +316,17 @@ func main() {
slog.Debug("no URL scheme request to handle")
}
+ go func() {
+ slog.Debug("waiting for ollama server to be ready")
+ if err := ui.WaitForServer(ctx, 10*time.Second); err != nil {
+ slog.Warn("ollama server not ready, continuing anyway", "error", err)
+ }
+
+ if _, err := uiServer.UserData(ctx); err != nil {
+ slog.Warn("failed to load user data", "error", err)
+ }
+ }()
+
osRun(cancel, hasCompletedFirstRun, startHidden)
slog.Info("shutting down desktop server")
@@ -361,7 +368,7 @@ func checkUserLoggedIn(uiServerPort int) bool {
return false
}
- resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/api/v1/me", uiServerPort))
+ resp, err := http.Post(fmt.Sprintf("http://127.0.0.1:%d/api/me", uiServerPort), "application/json", nil)
if err != nil {
slog.Debug("failed to call local auth endpoint", "error", err)
return false
@@ -397,8 +404,8 @@ func checkUserLoggedIn(uiServerPort int) bool {
// handleConnectURLScheme fetches the connect URL and opens it in the browser
func handleConnectURLScheme() {
if checkUserLoggedIn(uiServerPort) {
- slog.Info("user is already logged in, opening settings instead")
- sendUIRequestMessage("/")
+ slog.Info("user is already logged in, opening app instead")
+ showWindow(wv.webview.Window())
return
}
@@ -434,37 +441,30 @@ func openInBrowser(url string) {
}
}
-// parseURLScheme parses an ollama:// URL and returns whether it's a connect URL and the UI path
-func parseURLScheme(urlSchemeRequest string) (isConnect bool, uiPath string, err error) {
+// parseURLScheme parses an ollama:// URL and validates it
+// Supports: ollama:// (open app) and ollama://connect (OAuth)
+func parseURLScheme(urlSchemeRequest string) (isConnect bool, err error) {
parsedURL, err := url.Parse(urlSchemeRequest)
if err != nil {
- return false, "", err
+ return false, fmt.Errorf("invalid URL: %w", err)
}
// Check if this is a connect URL
if parsedURL.Host == "connect" || strings.TrimPrefix(parsedURL.Path, "/") == "connect" {
- return true, "", nil
+ return true, nil
}
- // Extract the UI path
- path := "/"
- if parsedURL.Path != "" && parsedURL.Path != "/" {
- // For URLs like ollama:///settings, use the path directly
- path = parsedURL.Path
- } else if parsedURL.Host != "" {
- // For URLs like ollama://settings (without triple slash),
- // the "settings" part is parsed as the host, not the path.
- // We need to convert it to a path by prepending "/"
- // This also handles ollama://settings/ where Windows adds a trailing slash
- path = "/" + parsedURL.Host
+ // Allow bare ollama:// or ollama:/// to open the app
+ if (parsedURL.Host == "" && parsedURL.Path == "") || parsedURL.Path == "/" {
+ return false, nil
}
- return false, path, nil
+ return false, fmt.Errorf("unsupported ollama:// URL path: %s", urlSchemeRequest)
}
// handleURLSchemeInCurrentInstance processes URL scheme requests in the current instance
func handleURLSchemeInCurrentInstance(urlSchemeRequest string) {
- isConnect, uiPath, err := parseURLScheme(urlSchemeRequest)
+ isConnect, err := parseURLScheme(urlSchemeRequest)
if err != nil {
slog.Error("failed to parse URL scheme request", "url", urlSchemeRequest, "error", err)
return
@@ -473,6 +473,8 @@ func handleURLSchemeInCurrentInstance(urlSchemeRequest string) {
if isConnect {
handleConnectURLScheme()
} else {
- sendUIRequestMessage(uiPath)
+ if wv.webview != nil {
+ showWindow(wv.webview.Window())
+ }
}
}
diff --git a/app/cmd/app/app_darwin.go b/app/cmd/app/app_darwin.go
index 2018ce8e409..8e886a124bd 100644
--- a/app/cmd/app/app_darwin.go
+++ b/app/cmd/app/app_darwin.go
@@ -191,13 +191,6 @@ func LaunchNewApp() {
C.launchApp(appName)
}
-// Send a request to the main app thread to load a UI page
-func sendUIRequestMessage(path string) {
- p := C.CString(path)
- defer C.free(unsafe.Pointer(p))
- C.uiRequest(p)
-}
-
func registerLaunchAgent(hasCompletedFirstRun bool) {
// Remove any stale Login Item registrations
C.unregisterSelfFromLoginItem()
diff --git a/app/cmd/app/app_darwin.m b/app/cmd/app/app_darwin.m
index 4e1d52f76df..d5095ab25a1 100644
--- a/app/cmd/app/app_darwin.m
+++ b/app/cmd/app/app_darwin.m
@@ -24,27 +24,14 @@ - (void)application:(NSApplication *)application openURLs:(NSArray *)ur
for (NSURL *url in urls) {
if ([url.scheme isEqualToString:@"ollama"]) {
NSString *path = url.path;
- if (!path || [path isEqualToString:@""]) {
- // For URLs like ollama://settings (without triple slash),
- // the "settings" part is parsed as the host, not the path.
- // We need to convert it to a path by prepending "/"
- if (url.host && ![url.host isEqualToString:@""]) {
- path = [@"/" stringByAppendingString:url.host];
- } else {
- path = @"/";
- }
- }
-
- if ([path isEqualToString:@"/connect"] || [url.host isEqualToString:@"connect"]) {
+
+ if (path && ([path isEqualToString:@"/connect"] || [url.host isEqualToString:@"connect"])) {
// Special case: handle connect by opening browser instead of app
handleConnectURL();
} else {
// Set app to be active and visible
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp activateIgnoringOtherApps:YES];
-
- // Open the path with the UI
- [self uiRequest:path];
}
break;
@@ -260,7 +247,7 @@ - (void)aboutOllama {
}
- (void)openHelp:(id)sender {
- NSURL *url = [NSURL URLWithString:@"https://github.com/ollama/ollama/tree/main/docs"];
+ NSURL *url = [NSURL URLWithString:@"https://docs.ollama.com/"];
[[NSWorkspace sharedWorkspace] openURL:url];
}
diff --git a/app/cmd/app/app_windows.go b/app/cmd/app/app_windows.go
index 51cf1f9233e..9caeb178ae9 100644
--- a/app/cmd/app/app_windows.go
+++ b/app/cmd/app/app_windows.go
@@ -138,7 +138,7 @@ func (app *appCallbacks) HandleURLScheme(urlScheme string) {
// handleURLSchemeRequest processes URL scheme requests from other instances
func handleURLSchemeRequest(urlScheme string) {
- isConnect, uiPath, err := parseURLScheme(urlScheme)
+ isConnect, err := parseURLScheme(urlScheme)
if err != nil {
slog.Error("failed to parse URL scheme request", "url", urlScheme, "error", err)
return
@@ -147,7 +147,9 @@ func handleURLSchemeRequest(urlScheme string) {
if isConnect {
handleConnectURLScheme()
} else {
- sendUIRequestMessage(uiPath)
+ if wv.webview != nil {
+ showWindow(wv.webview.Window())
+ }
}
}
@@ -261,11 +263,6 @@ func createLoginShortcut() error {
return nil
}
-// Send a request to the main app thread to load a UI page
-func sendUIRequestMessage(path string) {
- wintray.SendUIRequestMessage(path)
-}
-
func LaunchNewApp() {
}
diff --git a/app/dialog/cocoa/dlg.m b/app/dialog/cocoa/dlg.m
index e90098e74f0..35851978de5 100644
--- a/app/dialog/cocoa/dlg.m
+++ b/app/dialog/cocoa/dlg.m
@@ -169,37 +169,47 @@ - (DlgResult)load {
}
NSArray* urls = [panel URLs];
- if(self->params->allowMultiple && [urls count] >= 1) {
+ if([urls count] == 0) {
+ return DLG_CANCEL;
+ }
+
+ if(self->params->allowMultiple) {
// For multiple files, we need to return all paths separated by null bytes
char* bufPtr = self->params->buf;
int remainingBuf = self->params->nbuf;
- // Calculate total required buffer size first
- int totalSize = 0;
- for(NSURL* url in urls) {
- char tempBuf[PATH_MAX];
- if(![url getFileSystemRepresentation:tempBuf maxLength:PATH_MAX]) {
- return DLG_URLFAIL;
- }
- totalSize += strlen(tempBuf) + 1; // +1 for null terminator
- }
- totalSize += 1; // Final null terminator
-
- if(totalSize > self->params->nbuf) {
- // Not enough buffer space
- return DLG_URLFAIL;
- }
-
- // Now actually copy the paths (we know we have space)
- bufPtr = self->params->buf;
- for(NSURL* url in urls) {
- char tempBuf[PATH_MAX];
- [url getFileSystemRepresentation:tempBuf maxLength:PATH_MAX];
- int pathLen = strlen(tempBuf);
- strcpy(bufPtr, tempBuf);
- bufPtr += pathLen + 1;
- }
- *bufPtr = '\0'; // Final null terminator
+ // Calculate total required buffer size first
+ int totalSize = 0;
+ for(NSURL* url in urls) {
+ char tempBuf[PATH_MAX];
+ if(![url getFileSystemRepresentation:tempBuf maxLength:PATH_MAX]) {
+ return DLG_URLFAIL;
+ }
+ totalSize += strlen(tempBuf) + 1; // +1 for null terminator
+ }
+ totalSize += 1; // Final null terminator
+
+ if(totalSize > self->params->nbuf) {
+ // Not enough buffer space
+ return DLG_URLFAIL;
+ }
+
+ // Now actually copy the paths (we know we have space)
+ bufPtr = self->params->buf;
+ for(NSURL* url in urls) {
+ char tempBuf[PATH_MAX];
+ [url getFileSystemRepresentation:tempBuf maxLength:PATH_MAX];
+ int pathLen = strlen(tempBuf);
+ strcpy(bufPtr, tempBuf);
+ bufPtr += pathLen + 1;
+ }
+ *bufPtr = '\0'; // Final null terminator
+ } else {
+ // Single file/directory selection - write path to buffer
+ NSURL* url = [urls firstObject];
+ if(![url getFileSystemRepresentation:self->params->buf maxLength:self->params->nbuf]) {
+ return DLG_URLFAIL;
+ }
}
return DLG_OK;
diff --git a/app/dialog/dlgs_windows.go b/app/dialog/dlgs_windows.go
index c5b175caa6d..51ba9ee69dd 100644
--- a/app/dialog/dlgs_windows.go
+++ b/app/dialog/dlgs_windows.go
@@ -15,7 +15,7 @@ const multiFileBufferSize = w32.MAX_PATH * 10
type WinDlgError int
func (e WinDlgError) Error() string {
- return fmt.Sprintf("CommDlgExtendedError: %#x", e)
+ return fmt.Sprintf("CommDlgExtendedError: %#x", int(e))
}
func err() error {
diff --git a/app/server/server.go b/app/server/server.go
index 64b96b1fdfd..2e0c2d1edad 100644
--- a/app/server/server.go
+++ b/app/server/server.go
@@ -224,9 +224,7 @@ func (s *Server) cmd(ctx context.Context) (*exec.Cmd, error) {
if _, err := os.Stat(settings.Models); err == nil {
env["OLLAMA_MODELS"] = settings.Models
} else {
- slog.Warn("models path not accessible, clearing models setting", "path", settings.Models, "err", err)
- settings.Models = ""
- s.store.SetSettings(settings)
+ slog.Warn("models path not accessible, using default", "path", settings.Models, "err", err)
}
}
if settings.ContextLength > 0 {
diff --git a/app/ui/app/codegen/gotypes.gen.ts b/app/ui/app/codegen/gotypes.gen.ts
index a077c854670..0bf86f2b419 100644
--- a/app/ui/app/codegen/gotypes.gen.ts
+++ b/app/ui/app/codegen/gotypes.gen.ts
@@ -469,26 +469,24 @@ export class HealthResponse {
}
export class User {
id: string;
- name: string;
email: string;
- avatarURL: string;
- plan: string;
- bio: string;
- firstName: string;
- lastName: string;
- overThreshold: boolean;
+ name: string;
+ bio?: string;
+ avatarurl?: string;
+ firstname?: string;
+ lastname?: string;
+ plan?: string;
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
- this.name = source["name"];
this.email = source["email"];
- this.avatarURL = source["avatarURL"];
- this.plan = source["plan"];
+ this.name = source["name"];
this.bio = source["bio"];
- this.firstName = source["firstName"];
- this.lastName = source["lastName"];
- this.overThreshold = source["overThreshold"];
+ this.avatarurl = source["avatarurl"];
+ this.firstname = source["firstname"];
+ this.lastname = source["lastname"];
+ this.plan = source["plan"];
}
}
export class Attachment {
diff --git a/app/ui/app/src/api.ts b/app/ui/app/src/api.ts
index 4158bafcc4d..273850d6b04 100644
--- a/app/ui/app/src/api.ts
+++ b/app/ui/app/src/api.ts
@@ -15,6 +15,7 @@ import {
import { parseJsonlFromResponse } from "./util/jsonl-parsing";
import { ollamaClient as ollama } from "./lib/ollama-client";
import type { ModelResponse } from "ollama/browser";
+import { API_BASE, OLLAMA_DOT_COM } from "./lib/config";
// Extend Model class with utility methods
declare module "@/gotypes" {
@@ -26,9 +27,6 @@ declare module "@/gotypes" {
Model.prototype.isCloud = function (): boolean {
return this.model.endsWith("cloud");
};
-
-const API_BASE = import.meta.env.DEV ? "http://127.0.0.1:3001" : "";
-
// Helper function to convert Uint8Array to base64
function uint8ArrayToBase64(uint8Array: Uint8Array): string {
const chunkSize = 0x8000; // 32KB chunks to avoid stack overflow
@@ -43,44 +41,50 @@ function uint8ArrayToBase64(uint8Array: Uint8Array): string {
}
export async function fetchUser(): Promise {
- try {
- const response = await fetch(`${API_BASE}/api/v1/me`, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- });
+ const response = await fetch(`${API_BASE}/api/me`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
- if (response.ok) {
- const userData: User = await response.json();
- return userData;
+ if (response.ok) {
+ const userData: User = await response.json();
+
+ if (userData.avatarurl && !userData.avatarurl.startsWith("http")) {
+ userData.avatarurl = `${OLLAMA_DOT_COM}${userData.avatarurl}`;
}
- return null;
- } catch (error) {
- console.error("Error fetching user:", error);
+ return userData;
+ }
+
+ if (response.status === 401 || response.status === 403) {
return null;
}
+
+ throw new Error(`Failed to fetch user: ${response.status}`);
}
export async function fetchConnectUrl(): Promise {
- const response = await fetch(`${API_BASE}/api/v1/connect`, {
- method: "GET",
+ const response = await fetch(`${API_BASE}/api/me`, {
+ method: "POST",
headers: {
"Content-Type": "application/json",
},
});
- if (!response.ok) {
- throw new Error("Failed to fetch connect URL");
+ if (response.status === 401) {
+ const data = await response.json();
+ if (data.signin_url) {
+ return data.signin_url;
+ }
}
- const data = await response.json();
- return data.connect_url;
+ throw new Error("Failed to fetch connect URL");
}
export async function disconnectUser(): Promise {
- const response = await fetch(`${API_BASE}/api/v1/disconnect`, {
+ const response = await fetch(`${API_BASE}/api/signout`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -205,12 +209,10 @@ export async function* sendMessage(
data: uint8ArrayToBase64(att.data),
}));
- // Only send think parameter when actually requesting thinking
- // Don't send false as it causes issues with some providers
+ // Send think parameter when it's explicitly set (true, false, or a non-empty string).
const shouldSendThink =
think !== undefined &&
- ((typeof think === "boolean" && think) ||
- (typeof think === "string" && think !== ""));
+ (typeof think === "boolean" || (typeof think === "string" && think !== ""));
const response = await fetch(`${API_BASE}/api/v1/chat/${chatId}`, {
method: "POST",
@@ -392,7 +394,8 @@ export async function getInferenceCompute(): Promise {
export async function fetchHealth(): Promise {
try {
- const response = await fetch(`${API_BASE}/api/v1/health`, {
+ // Use the /api/version endpoint as a health check
+ const response = await fetch(`${API_BASE}/api/version`, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -401,7 +404,8 @@ export async function fetchHealth(): Promise {
if (response.ok) {
const data = await response.json();
- return data.healthy || false;
+ // If we get a version back, the server is healthy
+ return !!data.version;
}
return false;
diff --git a/app/ui/app/src/components/Settings.tsx b/app/ui/app/src/components/Settings.tsx
index c56a97b359e..057f7477d46 100644
--- a/app/ui/app/src/components/Settings.tsx
+++ b/app/ui/app/src/components/Settings.tsx
@@ -299,9 +299,9 @@ export default function Settings() {
- {user?.avatarURL && (
+ {user?.avatarurl && (
{
diff --git a/app/ui/app/src/components/Thinking.tsx b/app/ui/app/src/components/Thinking.tsx
index 7ab23e726d6..e82364240d8 100644
--- a/app/ui/app/src/components/Thinking.tsx
+++ b/app/ui/app/src/components/Thinking.tsx
@@ -50,21 +50,33 @@ export default function Thinking({
// Position content to show bottom when collapsed
useEffect(() => {
if (isCollapsed && contentRef.current && wrapperRef.current) {
- const contentHeight = contentRef.current.scrollHeight;
- const wrapperHeight = wrapperRef.current.clientHeight;
- if (contentHeight > wrapperHeight) {
- const translateY = -(contentHeight - wrapperHeight);
- contentRef.current.style.transform = `translateY(${translateY}px)`;
- setHasOverflow(true);
- } else {
- setHasOverflow(false);
- }
+ requestAnimationFrame(() => {
+ if (!contentRef.current || !wrapperRef.current) return;
+
+ const contentHeight = contentRef.current.scrollHeight;
+ const wrapperHeight = wrapperRef.current.clientHeight;
+ if (contentHeight > wrapperHeight) {
+ const translateY = -(contentHeight - wrapperHeight);
+ contentRef.current.style.transform = `translateY(${translateY}px)`;
+ setHasOverflow(true);
+ } else {
+ contentRef.current.style.transform = "translateY(0)";
+ setHasOverflow(false);
+ }
+ });
} else if (contentRef.current) {
contentRef.current.style.transform = "translateY(0)";
setHasOverflow(false);
}
}, [thinking, isCollapsed]);
+ useEffect(() => {
+ if (activelyThinking && wrapperRef.current && !isCollapsed) {
+ // When expanded and actively thinking, scroll to bottom
+ wrapperRef.current.scrollTop = wrapperRef.current.scrollHeight;
+ }
+ }, [thinking, activelyThinking, isCollapsed]);
+
const handleToggle = () => {
setIsCollapsed(!isCollapsed);
setHasUserInteracted(true);
diff --git a/app/ui/app/src/hooks/useChats.ts b/app/ui/app/src/hooks/useChats.ts
index b7517253380..410a80e7ea4 100644
--- a/app/ui/app/src/hooks/useChats.ts
+++ b/app/ui/app/src/hooks/useChats.ts
@@ -7,6 +7,7 @@ import { createQueryBatcher } from "./useQueryBatcher";
import { useRefetchModels } from "./useModels";
import { useStreamingContext } from "@/contexts/StreamingContext";
import { useSettings } from "./useSettings";
+import { getModelCapabilities } from "@/api";
export const useChats = () => {
return useQuery({
@@ -606,6 +607,24 @@ export const useSendMessage = (chatId: string) => {
queryClient.setQueryData(["staleModels"], newStaleMap);
queryClient.invalidateQueries({ queryKey: ["models"] });
+
+ // Fetch fresh capabilities for the downloaded model
+ getModelCapabilities(selectedModel.model)
+ .then((capabilities) => {
+ queryClient.setQueryData(
+ ["modelCapabilities", selectedModel.model],
+ capabilities,
+ );
+ })
+ .catch((error) => {
+ console.error(
+ "Failed to fetch capabilities after download:",
+ error,
+ );
+ queryClient.invalidateQueries({
+ queryKey: ["modelCapabilities", selectedModel.model],
+ });
+ });
}
break;
}
diff --git a/app/ui/app/src/hooks/useDownloadModel.ts b/app/ui/app/src/hooks/useDownloadModel.ts
deleted file mode 100644
index aa69edec1ec..00000000000
--- a/app/ui/app/src/hooks/useDownloadModel.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { useState } from "react";
-import { pullModel } from "@/api";
-import { useSelectedModel } from "./useSelectedModel";
-import { useSettings } from "./useSettings";
-
-interface DownloadProgress {
- status: string;
- digest?: string;
- total?: number;
- completed?: number;
- done?: boolean;
-}
-
-export function useDownloadModel(chatId?: string) {
- const queryClient = useQueryClient();
- const { selectedModel } = useSelectedModel(chatId);
- const { setSettings } = useSettings();
- const [downloadProgress, setDownloadProgress] =
- useState(null);
- const [abortController, setAbortController] =
- useState(null);
- const [downloadingChatIds, setDownloadingChatIds] = useState>(
- new Set(),
- );
-
- const mutation = useMutation({
- mutationFn: async (modelName: string) => {
- const controller = new AbortController();
- setAbortController(controller);
- setDownloadProgress({ status: "Starting download..." });
- if (chatId) {
- setDownloadingChatIds((prev) => new Set(prev).add(chatId));
- }
-
- try {
- for await (const progress of pullModel(modelName, controller.signal)) {
- setDownloadProgress(progress);
-
- if (progress.status === "success") {
- // Update selected model to indicate it's now available locally
- if (selectedModel && selectedModel.model === modelName) {
- setSettings({ SelectedModel: modelName });
- }
- // Invalidate models query to refresh the list
- await queryClient.invalidateQueries({ queryKey: ["models"] });
- break;
- }
- }
- } finally {
- setAbortController(null);
- if (chatId) {
- setDownloadingChatIds((prev) => {
- const newSet = new Set(prev);
- newSet.delete(chatId);
- return newSet;
- });
- }
- }
- },
- onSuccess: () => {
- setDownloadProgress(null);
- if (chatId) {
- setDownloadingChatIds((prev) => {
- const newSet = new Set(prev);
- newSet.delete(chatId);
- return newSet;
- });
- }
- },
- onError: (error: Error) => {
- const status =
- error.name === "AbortError" ? "Download cancelled" : "Download failed";
- setDownloadProgress({ status, done: true });
-
- // Clear error message after delay
- const delay = error.name === "AbortError" ? 1500 : 3000;
- setTimeout(() => {
- setDownloadProgress(null);
- if (chatId) {
- setDownloadingChatIds((prev) => {
- const newSet = new Set(prev);
- newSet.delete(chatId);
- return newSet;
- });
- }
- }, delay);
- },
- });
-
- const cancelDownload = () => {
- if (abortController) {
- abortController.abort();
- setAbortController(null);
- if (chatId) {
- setDownloadingChatIds((prev) => {
- const newSet = new Set(prev);
- newSet.delete(chatId);
- return newSet;
- });
- }
- }
- };
-
- return {
- downloadModel: mutation.mutate,
- isDownloading:
- mutation.isPending && chatId ? downloadingChatIds.has(chatId) : false,
- downloadProgress:
- chatId && downloadingChatIds.has(chatId) ? downloadProgress : null,
- error: mutation.error,
- cancelDownload,
- };
-}
diff --git a/app/ui/app/src/hooks/useUser.ts b/app/ui/app/src/hooks/useUser.ts
index 5f7a4dade1f..b4e6698eb63 100644
--- a/app/ui/app/src/hooks/useUser.ts
+++ b/app/ui/app/src/hooks/useUser.ts
@@ -1,29 +1,20 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-import { useEffect, useState } from "react";
import { fetchUser, fetchConnectUrl, disconnectUser } from "@/api";
export function useUser() {
const queryClient = useQueryClient();
- const [initialDataLoaded, setInitialDataLoaded] = useState(false);
-
- // Wait for initial data to be loaded
- useEffect(() => {
- const initialPromise = window.__initialUserDataPromise;
- if (initialPromise) {
- initialPromise.finally(() => {
- setInitialDataLoaded(true);
- });
- } else {
- setInitialDataLoaded(true);
- }
- }, []);
const userQuery = useQuery({
queryKey: ["user"],
- queryFn: () => fetchUser(),
+ queryFn: async () => {
+ const result = await fetchUser();
+ return result;
+ },
staleTime: 5 * 60 * 1000, // Consider data stale after 5 minutes
gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
- initialData: null, // Start with null to prevent flashing
+ retry: 10,
+ retryDelay: (attemptIndex) => Math.min(500 * attemptIndex, 2000),
+ refetchOnMount: true, // Always fetch when component mounts
});
// Mutation to refresh user data
@@ -49,14 +40,15 @@ export function useUser() {
},
});
+ const isLoading = userQuery.isLoading || userQuery.isFetching;
+ const isAuthenticated = Boolean(userQuery.data?.name);
+
return {
user: userQuery.data,
- isLoading:
- !initialDataLoaded ||
- (userQuery.isLoading && userQuery.data === undefined), // Show loading until initial data is loaded
+ isLoading,
isError: userQuery.isError,
error: userQuery.error,
- isAuthenticated: Boolean(userQuery.data?.name),
+ isAuthenticated,
refreshUser: refreshUser.mutate,
isRefreshing: refreshUser.isPending,
refetchUser: userQuery.refetch,
diff --git a/app/ui/app/src/lib/config.ts b/app/ui/app/src/lib/config.ts
new file mode 100644
index 00000000000..7c5385d743c
--- /dev/null
+++ b/app/ui/app/src/lib/config.ts
@@ -0,0 +1,13 @@
+// API configuration
+const DEV_API_URL = "http://127.0.0.1:3001";
+
+// Base URL for fetch API calls (can be relative in production)
+export const API_BASE = import.meta.env.DEV ? DEV_API_URL : "";
+
+// Full host URL for Ollama client (needs full origin in production)
+export const OLLAMA_HOST = import.meta.env.DEV
+ ? DEV_API_URL
+ : window.location.origin;
+
+export const OLLAMA_DOT_COM =
+ import.meta.env.VITE_OLLAMA_DOT_COM_URL || "https://ollama.com";
diff --git a/app/ui/app/src/lib/highlighter.ts b/app/ui/app/src/lib/highlighter.ts
index 279bc746c70..2912fb358f7 100644
--- a/app/ui/app/src/lib/highlighter.ts
+++ b/app/ui/app/src/lib/highlighter.ts
@@ -147,6 +147,7 @@ export const highlighterPromise = createHighlighter({
"c",
"cpp",
"sql",
+ "swift",
"yaml",
"markdown",
],
diff --git a/app/ui/app/src/lib/ollama-client.ts b/app/ui/app/src/lib/ollama-client.ts
index 9e931614d0f..50915a0babb 100644
--- a/app/ui/app/src/lib/ollama-client.ts
+++ b/app/ui/app/src/lib/ollama-client.ts
@@ -1,4 +1,5 @@
import { Ollama } from "ollama/browser";
+import { OLLAMA_HOST } from "./config";
let _ollamaClient: Ollama | null = null;
@@ -6,7 +7,7 @@ export const ollamaClient = new Proxy({} as Ollama, {
get(_target, prop) {
if (!_ollamaClient) {
_ollamaClient = new Ollama({
- host: window.location.origin,
+ host: OLLAMA_HOST,
});
}
const value = _ollamaClient[prop as keyof Ollama];
diff --git a/app/ui/app/src/main.tsx b/app/ui/app/src/main.tsx
index 1ffe37ef664..3e325a3caea 100644
--- a/app/ui/app/src/main.tsx
+++ b/app/ui/app/src/main.tsx
@@ -5,13 +5,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { routeTree } from "./routeTree.gen";
import { fetchUser } from "./api";
import { StreamingProvider } from "./contexts/StreamingContext";
-import { User } from "@/gotypes";
-
-declare global {
- interface Window {
- __initialUserDataPromise?: Promise;
- }
-}
const queryClient = new QueryClient({
defaultOptions: {
@@ -24,27 +17,11 @@ const queryClient = new QueryClient({
},
});
-// Track initial user data fetch
-let initialUserDataPromise: Promise | null = null;
-
-// Initialize user data on app startup
-const initializeUserData = async () => {
- try {
- const userData = await fetchUser();
+fetchUser().then((userData) => {
+ if (userData) {
queryClient.setQueryData(["user"], userData);
- return userData;
- } catch (error) {
- console.error("Error initializing user data:", error);
- queryClient.setQueryData(["user"], null);
- return null;
}
-};
-
-// Start initialization immediately and track the promise
-initialUserDataPromise = initializeUserData();
-
-// Export the promise so hooks can await it
-window.__initialUserDataPromise = initialUserDataPromise;
+});
const router = createRouter({
routeTree,
diff --git a/app/ui/responses/types.go b/app/ui/responses/types.go
index 438dd55e40d..2da6623fdc6 100644
--- a/app/ui/responses/types.go
+++ b/app/ui/responses/types.go
@@ -101,15 +101,14 @@ type HealthResponse struct {
}
type User struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Email string `json:"email"`
- AvatarURL string `json:"avatarURL"`
- Plan string `json:"plan"`
- Bio string `json:"bio"`
- FirstName string `json:"firstName"`
- LastName string `json:"lastName"`
- OverThreshold bool `json:"overThreshold"`
+ ID string `json:"id"`
+ Email string `json:"email"`
+ Name string `json:"name"`
+ Bio string `json:"bio,omitempty"`
+ AvatarURL string `json:"avatarurl,omitempty"`
+ FirstName string `json:"firstname,omitempty"`
+ LastName string `json:"lastname,omitempty"`
+ Plan string `json:"plan,omitempty"`
}
type Attachment struct {
diff --git a/app/ui/ui.go b/app/ui/ui.go
index 1d0e2579676..0b32f917e68 100644
--- a/app/ui/ui.go
+++ b/app/ui/ui.go
@@ -12,18 +12,17 @@ import (
"log/slog"
"net/http"
"net/http/httputil"
- "net/url"
"os"
"runtime"
"runtime/debug"
"slices"
"strconv"
"strings"
+ "sync"
"time"
"github.com/google/uuid"
"github.com/ollama/ollama/api"
- "github.com/ollama/ollama/app/auth"
"github.com/ollama/ollama/app/server"
"github.com/ollama/ollama/app/store"
"github.com/ollama/ollama/app/tools"
@@ -118,40 +117,66 @@ func (s *Server) log() *slog.Logger {
// ollamaProxy creates a reverse proxy handler to the Ollama server
func (s *Server) ollamaProxy() http.Handler {
- ollamaHost := os.Getenv("OLLAMA_HOST")
- if ollamaHost == "" {
- ollamaHost = "http://127.0.0.1:11434"
- }
+ var (
+ proxy http.Handler
+ proxyMu sync.Mutex
+ )
- if !strings.HasPrefix(ollamaHost, "http://") && !strings.HasPrefix(ollamaHost, "https://") {
- ollamaHost = "http://" + ollamaHost
- }
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ proxyMu.Lock()
+ p := proxy
+ proxyMu.Unlock()
+
+ if p == nil {
+ proxyMu.Lock()
+ if proxy == nil {
+ var err error
+ for i := range 2 {
+ if i > 0 {
+ s.log().Warn("ollama server not ready, retrying", "attempt", i+1)
+ time.Sleep(1 * time.Second)
+ }
- target, err := url.Parse(ollamaHost)
- if err != nil {
- s.log().Error("failed to parse OLLAMA_HOST", "error", err, "host", ollamaHost)
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- http.Error(w, "failed to configure proxy", http.StatusInternalServerError)
- })
- }
+ err = WaitForServer(context.Background(), 10*time.Second)
+ if err == nil {
+ break
+ }
+ }
- s.log().Info("configuring ollama proxy", "target", target.String())
+ if err != nil {
+ proxyMu.Unlock()
+ s.log().Error("ollama server not ready after retries", "error", err)
+ http.Error(w, "Ollama server is not ready", http.StatusServiceUnavailable)
+ return
+ }
- proxy := httputil.NewSingleHostReverseProxy(target)
+ target := envconfig.Host()
+ s.log().Info("configuring ollama proxy", "target", target.String())
- originalDirector := proxy.Director
- proxy.Director = func(req *http.Request) {
- originalDirector(req)
- req.Host = target.Host
- s.log().Debug("proxying request", "method", req.Method, "path", req.URL.Path, "target", target.Host)
- }
+ newProxy := httputil.NewSingleHostReverseProxy(target)
- proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
- s.log().Error("proxy error", "error", err, "path", r.URL.Path, "target", target.String())
- http.Error(w, "proxy error: "+err.Error(), http.StatusBadGateway)
- }
+ originalDirector := newProxy.Director
+ newProxy.Director = func(req *http.Request) {
+ originalDirector(req)
+ req.Host = target.Host
+ s.log().Debug("proxying request", "method", req.Method, "path", req.URL.Path, "target", target.Host)
+ }
- return proxy
+ newProxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
+ s.log().Error("proxy error", "error", err, "path", r.URL.Path, "target", target.String())
+ http.Error(w, "proxy error: "+err.Error(), http.StatusBadGateway)
+ }
+
+ proxy = newProxy
+ p = newProxy
+ } else {
+ p = proxy
+ }
+ proxyMu.Unlock()
+ }
+
+ p.ServeHTTP(w, r)
+ })
}
type errHandlerFunc func(http.ResponseWriter, *http.Request) error
@@ -264,11 +289,10 @@ func (s *Server) Handler() http.Handler {
ollamaProxy := s.ollamaProxy()
mux.Handle("GET /api/tags", ollamaProxy)
mux.Handle("POST /api/show", ollamaProxy)
-
- mux.Handle("GET /api/v1/me", handle(s.me))
- mux.Handle("POST /api/v1/disconnect", handle(s.disconnect))
- mux.Handle("GET /api/v1/connect", handle(s.connectURL))
- mux.Handle("GET /api/v1/health", handle(s.health))
+ mux.Handle("GET /api/version", ollamaProxy)
+ mux.Handle("HEAD /api/version", ollamaProxy)
+ mux.Handle("POST /api/me", ollamaProxy)
+ mux.Handle("POST /api/signout", ollamaProxy)
// React app - catch all non-API routes and serve the React app
mux.Handle("GET /", s.appHandler())
@@ -338,7 +362,7 @@ func (s *Server) doSelfSigned(ctx context.Context, method, path string) (*http.R
}
// UserData fetches user data from ollama.com API for the current ollama key
-func (s *Server) UserData(ctx context.Context) (*responses.User, error) {
+func (s *Server) UserData(ctx context.Context) (*api.UserResponse, error) {
resp, err := s.doSelfSigned(ctx, http.MethodPost, "/api/me")
if err != nil {
return nil, fmt.Errorf("failed to call ollama.com/api/me: %w", err)
@@ -349,7 +373,7 @@ func (s *Server) UserData(ctx context.Context) (*responses.User, error) {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
- var user responses.User
+ var user api.UserResponse
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, fmt.Errorf("failed to parse user response: %w", err)
}
@@ -368,29 +392,27 @@ func (s *Server) UserData(ctx context.Context) (*responses.User, error) {
return &user, nil
}
-func waitForServer(ctx context.Context) error {
- timeout := time.Now().Add(10 * time.Second)
- // TODO: this avoids an error on first load of the app
- // however we should either show a loading state or
- // wait for the Ollama server to be ready before redirecting
- for {
+// WaitForServer waits for the Ollama server to be ready
+func WaitForServer(ctx context.Context, timeout time.Duration) error {
+ deadline := time.Now().Add(timeout)
+ for time.Now().Before(deadline) {
c, err := api.ClientFromEnvironment()
if err != nil {
return err
}
if _, err := c.Version(ctx); err == nil {
- break
- }
- if time.Now().After(timeout) {
- return fmt.Errorf("timeout waiting for Ollama server to be ready")
+ slog.Debug("ollama server is ready")
+ return nil
}
time.Sleep(10 * time.Millisecond)
}
- return nil
+ return errors.New("timeout waiting for Ollama server to be ready")
}
func (s *Server) createChat(w http.ResponseWriter, r *http.Request) error {
- waitForServer(r.Context())
+ if err := WaitForServer(r.Context(), 10*time.Second); err != nil {
+ return err
+ }
id, err := uuid.NewV7()
if err != nil {
@@ -975,7 +997,7 @@ func (s *Server) chat(w http.ResponseWriter, r *http.Request) error {
for _, toolCall := range res.Message.ToolCalls {
// continues loop as tools were executed
toolsExecuted = true
- result, content, err := registry.Execute(ctx, toolCall.Function.Name, toolCall.Function.Arguments)
+ result, content, err := registry.Execute(ctx, toolCall.Function.Name, toolCall.Function.Arguments.ToMap())
if err != nil {
errContent := fmt.Sprintf("Error: %v", err)
toolErrMsg := store.NewMessage("tool", errContent, nil)
@@ -1438,129 +1460,6 @@ func (s *Server) settings(w http.ResponseWriter, r *http.Request) error {
})
}
-func (s *Server) me(w http.ResponseWriter, r *http.Request) error {
- if r.Method != http.MethodGet {
- http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
- return nil
- }
-
- user, err := s.UserData(r.Context())
- if err != nil {
- // If fetching from API fails, try to return cached user data if available
- if cachedUser, cacheErr := s.Store.User(); cacheErr == nil && cachedUser != nil {
- s.log().Info("API request failed, returning cached user data", "error", err)
- responseUser := &responses.User{
- Name: cachedUser.Name,
- Email: cachedUser.Email,
- Plan: cachedUser.Plan,
- }
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- return json.NewEncoder(w).Encode(responseUser)
- }
-
- s.log().Error("failed to get user data", "error", err)
- w.WriteHeader(http.StatusInternalServerError)
- return json.NewEncoder(w).Encode(responses.Error{
- Error: "failed to get user data",
- })
- }
-
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- return json.NewEncoder(w).Encode(user)
-}
-
-func (s *Server) disconnect(w http.ResponseWriter, r *http.Request) error {
- if r.Method != http.MethodPost {
- http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
- return nil
- }
-
- if err := s.Store.ClearUser(); err != nil {
- s.log().Warn("failed to clear cached user data", "error", err)
- }
-
- // Get the SSH public key to encode for the delete request
- pubKey, err := ollamaAuth.GetPublicKey()
- if err != nil {
- s.log().Error("failed to get public key", "error", err)
- w.WriteHeader(http.StatusInternalServerError)
- return json.NewEncoder(w).Encode(responses.Error{
- Error: "failed to get public key",
- })
- }
-
- // Encode the key using base64 URL encoding
- encodedKey := base64.RawURLEncoding.EncodeToString([]byte(pubKey))
-
- // Call the /api/user/keys/{encodedKey} endpoint with DELETE
- resp, err := s.doSelfSigned(r.Context(), http.MethodDelete, fmt.Sprintf("/api/user/keys/%s", encodedKey))
- if err != nil {
- s.log().Error("failed to call ollama.com/api/user/keys", "error", err)
- w.WriteHeader(http.StatusInternalServerError)
- return json.NewEncoder(w).Encode(responses.Error{
- Error: "failed to disconnect from ollama.com",
- })
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- s.log().Error("disconnect request failed", "status", resp.StatusCode)
- w.WriteHeader(http.StatusInternalServerError)
- return json.NewEncoder(w).Encode(responses.Error{
- Error: "failed to disconnect from ollama.com",
- })
- }
-
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- return json.NewEncoder(w).Encode(map[string]string{"status": "disconnected"})
-}
-
-func (s *Server) connectURL(w http.ResponseWriter, r *http.Request) error {
- if r.Method != http.MethodGet {
- http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
- return nil
- }
-
- connectURL, err := auth.BuildConnectURL(OllamaDotCom)
- if err != nil {
- s.log().Error("failed to build connect URL", "error", err)
- w.WriteHeader(http.StatusInternalServerError)
- return json.NewEncoder(w).Encode(responses.Error{
- Error: "failed to build connect URL",
- })
- }
-
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- return json.NewEncoder(w).Encode(map[string]string{
- "connect_url": connectURL,
- })
-}
-
-func (s *Server) health(w http.ResponseWriter, r *http.Request) error {
- if r.Method != http.MethodGet {
- http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
- return nil
- }
-
- healthy := false
- c, err := api.ClientFromEnvironment()
- if err == nil {
- if _, err := c.Version(r.Context()); err == nil {
- healthy = true
- }
- }
-
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- return json.NewEncoder(w).Encode(responses.HealthResponse{
- Healthy: healthy,
- })
-}
-
func (s *Server) getInferenceCompute(w http.ResponseWriter, r *http.Request) error {
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
defer cancel()
@@ -1659,13 +1558,13 @@ func convertToOllamaTool(toolSchema map[string]any) api.Tool {
tool.Function.Parameters.Type = "object"
tool.Function.Parameters.Required = []string{}
- tool.Function.Parameters.Properties = make(map[string]api.ToolProperty)
+ tool.Function.Parameters.Properties = api.NewToolPropertiesMap()
if schemaProps, ok := toolSchema["schema"].(map[string]any); ok {
tool.Function.Parameters.Type = getStringFromMap(schemaProps, "type", "object")
if props, ok := schemaProps["properties"].(map[string]any); ok {
- tool.Function.Parameters.Properties = make(map[string]api.ToolProperty)
+ tool.Function.Parameters.Properties = api.NewToolPropertiesMap()
for propName, propDef := range props {
if propMap, ok := propDef.(map[string]any); ok {
@@ -1673,7 +1572,7 @@ func convertToOllamaTool(toolSchema map[string]any) api.Tool {
Type: api.PropertyType{getStringFromMap(propMap, "type", "string")},
Description: getStringFromMap(propMap, "description", ""),
}
- tool.Function.Parameters.Properties[propName] = prop
+ tool.Function.Parameters.Properties.Set(propName, prop)
}
}
}
diff --git a/app/wintray/eventloop.go b/app/wintray/eventloop.go
index 15fbd0c31d9..dda433e28df 100644
--- a/app/wintray/eventloop.go
+++ b/app/wintray/eventloop.go
@@ -158,16 +158,16 @@ func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam ui
case uint32(UI_REQUEST_MSG_ID):
// Requests for the UI must always come from the main event thread
l := int(wParam)
- path := unsafe.String((*byte)(unsafe.Pointer(lParam)), l)
+ path := unsafe.String((*byte)(unsafe.Pointer(lParam)), l) //nolint:govet,gosec
t.app.UIRun(path)
case WM_COPYDATA:
// Handle URL scheme requests from other instances
if lParam != 0 {
- cds := (*COPYDATASTRUCT)(unsafe.Pointer(lParam))
- if cds.DwData == 1 { // Our identifier for URL scheme messages
+ cds := (*COPYDATASTRUCT)(unsafe.Pointer(lParam)) //nolint:govet,gosec
+ if cds.DwData == 1 { // Our identifier for URL scheme messages
// Convert the data back to string
data := make([]byte, cds.CbData)
- copy(data, (*[1 << 30]byte)(unsafe.Pointer(cds.LpData))[:cds.CbData:cds.CbData])
+ copy(data, (*[1 << 30]byte)(unsafe.Pointer(cds.LpData))[:cds.CbData:cds.CbData]) //nolint:govet,gosec
urlScheme := string(data)
handleURLSchemeRequest(urlScheme)
lResult = 1 // Return non-zero to indicate success
diff --git a/cmd/bench/README.md b/cmd/bench/README.md
new file mode 100644
index 00000000000..cf261dd0f3d
--- /dev/null
+++ b/cmd/bench/README.md
@@ -0,0 +1,115 @@
+Ollama Benchmark Tool
+---------------------
+
+A Go-based command-line tool for benchmarking Ollama models with configurable parameters and multiple output formats.
+
+## Features
+
+ * Benchmark multiple models in a single run
+ * Support for both text and image prompts
+ * Configurable generation parameters (temperature, max tokens, seed, etc.)
+ * Supports benchstat and CSV output formats
+ * Detailed performance metrics (prefill, generate, load, total durations)
+
+## Building from Source
+
+```
+go build -o ollama-bench bench.go
+./ollama-bench -model gpt-oss:20b -epochs 6 -format csv
+```
+
+Using Go Run (without building)
+
+```
+go run bench.go -model gpt-oss:20b -epochs 3
+```
+
+## Usage
+
+### Basic Example
+
+```
+./ollama-bench -model gemma3 -epochs 6
+```
+
+### Benchmark Multiple Models
+
+```
+./ollama-bench -model gemma3,gemma3n -epochs 6 -max-tokens 100 -p "Write me a short story" | tee gemma.bench
+benchstat -col /name gemma.bench
+```
+
+### With Image Prompt
+
+```
+./ollama-bench -model qwen3-vl -image photo.jpg -epochs 6 -max-tokens 100 -p "Describe this image"
+```
+
+### Advanced Example
+
+```
+./ollama-bench -model llama3 -epochs 10 -temperature 0.7 -max-tokens 500 -seed 42 -format csv -output results.csv
+```
+
+## Command Line Options
+
+| Option | Description | Default |
+|----------|-------------|---------|
+| -model | Comma-separated list of models to benchmark | (required) |
+| -epochs | Number of iterations per model | 1 |
+| -max-tokens | Maximum tokens for model response | 0 (unlimited) |
+| -temperature | Temperature parameter | 0.0 |
+| -seed | Random seed | 0 (random) |
+| -timeout | Timeout in seconds | 300 |
+| -p | Prompt text | "Write a long story." |
+| -image | Image file to include in prompt | |
+| -k | Keep-alive duration in seconds | 0 |
+| -format | Output format (benchstat, csv) | benchstat |
+| -output | Output file for results | "" (stdout) |
+| -v | Verbose mode | false |
+| -debug | Show debug information | false |
+
+## Output Formats
+
+### Markdown Format
+
+The default markdown format is suitable for copying and pasting into a GitHub issue and will look like:
+```
+ Model | Step | Count | Duration | nsPerToken | tokensPerSec |
+|-------|------|-------|----------|------------|--------------|
+| gpt-oss:20b | prefill | 124 | 30.006458ms | 241987.56 | 4132.44 |
+| gpt-oss:20b | generate | 200 | 2.646843954s | 13234219.77 | 75.56 |
+| gpt-oss:20b | load | 1 | 121.674208ms | - | - |
+| gpt-oss:20b | total | 1 | 2.861047625s | - | - |
+```
+
+### Benchstat Format
+
+Compatible with Go's benchstat tool for statistical analysis:
+
+```
+BenchmarkModel/name=gpt-oss:20b/step=prefill 128 78125.00 ns/token 12800.00 token/sec
+BenchmarkModel/name=gpt-oss:20b/step=generate 512 19531.25 ns/token 51200.00 token/sec
+BenchmarkModel/name=gpt-oss:20b/step=load 1 1500000000 ns/request
+```
+
+### CSV Format
+
+Machine-readable comma-separated values:
+
+```
+NAME,STEP,COUNT,NS_PER_COUNT,TOKEN_PER_SEC
+gpt-oss:20b,prefill,128,78125.00,12800.00
+gpt-oss:20b,generate,512,19531.25,51200.00
+gpt-oss:20b,load,1,1500000000,0
+```
+
+## Metrics Explained
+
+The tool reports four types of metrics for each model:
+
+ * prefill: Time spent processing the prompt
+ * generate: Time spent generating the response
+ * load: Model loading time (one-time cost)
+ * total: Total request duration
+
diff --git a/cmd/bench/bench.go b/cmd/bench/bench.go
new file mode 100644
index 00000000000..53721f877e1
--- /dev/null
+++ b/cmd/bench/bench.go
@@ -0,0 +1,321 @@
+package main
+
+import (
+ "cmp"
+ "context"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "runtime"
+ "slices"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/ollama/ollama/api"
+)
+
+type flagOptions struct {
+ models *string
+ epochs *int
+ maxTokens *int
+ temperature *float64
+ seed *int
+ timeout *int
+ prompt *string
+ imageFile *string
+ keepAlive *float64
+ format *string
+ outputFile *string
+ debug *bool
+ verbose *bool
+}
+
+type Metrics struct {
+ Model string
+ Step string
+ Count int
+ Duration time.Duration
+}
+
+var once sync.Once
+
+const DefaultPrompt = `Please write a descriptive story about a llama named Alonso who grows up to be President of the Land of Llamas. Include details about Alonso's childhood, adolescent years, and how he grew up to be a political mover and shaker. Write the story with a sense of whimsy.`
+
+func OutputMetrics(w io.Writer, format string, metrics []Metrics, verbose bool) {
+ switch format {
+ case "benchstat":
+ if verbose {
+ printHeader := func() {
+ fmt.Fprintf(w, "sysname: %s\n", runtime.GOOS)
+ fmt.Fprintf(w, "machine: %s\n", runtime.GOARCH)
+ }
+ once.Do(printHeader)
+ }
+ for _, m := range metrics {
+ if m.Step == "generate" || m.Step == "prefill" {
+ if m.Count > 0 {
+ nsPerToken := float64(m.Duration.Nanoseconds()) / float64(m.Count)
+ tokensPerSec := float64(m.Count) / (float64(m.Duration.Nanoseconds()) + 1e-12) * 1e9
+
+ fmt.Fprintf(w, "BenchmarkModel/name=%s/step=%s %d %.2f ns/token %.2f token/sec\n",
+ m.Model, m.Step, m.Count, nsPerToken, tokensPerSec)
+ } else {
+ fmt.Fprintf(w, "BenchmarkModel/name=%s/step=%s %d 0 ns/token 0 token/sec\n",
+ m.Model, m.Step, m.Count)
+ }
+ } else {
+ var suffix string
+ if m.Step == "load" {
+ suffix = "/step=load"
+ }
+ fmt.Fprintf(w, "BenchmarkModel/name=%s%s 1 %d ns/request\n",
+ m.Model, suffix, m.Duration.Nanoseconds())
+ }
+ }
+ case "csv":
+ printHeader := func() {
+ headings := []string{"NAME", "STEP", "COUNT", "NS_PER_COUNT", "TOKEN_PER_SEC"}
+ fmt.Fprintln(w, strings.Join(headings, ","))
+ }
+ once.Do(printHeader)
+
+ for _, m := range metrics {
+ if m.Step == "generate" || m.Step == "prefill" {
+ var nsPerToken float64
+ var tokensPerSec float64
+ if m.Count > 0 {
+ nsPerToken = float64(m.Duration.Nanoseconds()) / float64(m.Count)
+ tokensPerSec = float64(m.Count) / (float64(m.Duration.Nanoseconds()) + 1e-12) * 1e9
+ }
+ fmt.Fprintf(w, "%s,%s,%d,%.2f,%.2f\n", m.Model, m.Step, m.Count, nsPerToken, tokensPerSec)
+ } else {
+ fmt.Fprintf(w, "%s,%s,1,%d,0\n", m.Model, m.Step, m.Duration.Nanoseconds())
+ }
+ }
+ case "markdown":
+ printHeader := func() {
+ fmt.Fprintln(w, "| Model | Step | Count | Duration | nsPerToken | tokensPerSec |")
+ fmt.Fprintln(w, "|-------|------|-------|----------|------------|--------------|")
+ }
+ once.Do(printHeader)
+
+ for _, m := range metrics {
+ var nsPerToken, tokensPerSec float64
+ var nsPerTokenStr, tokensPerSecStr string
+
+ if m.Step == "generate" || m.Step == "prefill" {
+ nsPerToken = float64(m.Duration.Nanoseconds()) / float64(m.Count)
+ tokensPerSec = float64(m.Count) / (float64(m.Duration.Nanoseconds()) + 1e-12) * 1e9
+ nsPerTokenStr = fmt.Sprintf("%.2f", nsPerToken)
+ tokensPerSecStr = fmt.Sprintf("%.2f", tokensPerSec)
+ } else {
+ nsPerTokenStr = "-"
+ tokensPerSecStr = "-"
+ }
+
+ fmt.Fprintf(w, "| %s | %s | %d | %v | %s | %s |\n",
+ m.Model, m.Step, m.Count, m.Duration, nsPerTokenStr, tokensPerSecStr)
+ }
+ default:
+ fmt.Fprintf(os.Stderr, "Unknown output format '%s'\n", format)
+ }
+}
+
+func BenchmarkChat(fOpt flagOptions) error {
+ models := strings.Split(*fOpt.models, ",")
+
+ // todo - add multi-image support
+ var imgData api.ImageData
+ var err error
+ if *fOpt.imageFile != "" {
+ imgData, err = readImage(*fOpt.imageFile)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "ERROR: Couldn't read image '%s': %v\n", *fOpt.imageFile, err)
+ return err
+ }
+ }
+
+ if *fOpt.debug && imgData != nil {
+ fmt.Fprintf(os.Stderr, "Read file '%s'\n", *fOpt.imageFile)
+ }
+
+ client, err := api.ClientFromEnvironment()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "ERROR: Couldn't create ollama client: %v\n", err)
+ return err
+ }
+
+ var out io.Writer = os.Stdout
+ if fOpt.outputFile != nil && *fOpt.outputFile != "" {
+ f, err := os.OpenFile(*fOpt.outputFile, os.O_CREATE|os.O_WRONLY, 0o644)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "ERROR: cannot open output file %s: %v\n", *fOpt.outputFile, err)
+ return err
+ }
+ defer f.Close()
+ out = f
+ }
+
+ for _, model := range models {
+ for range *fOpt.epochs {
+ options := make(map[string]interface{})
+ if *fOpt.maxTokens > 0 {
+ options["num_predict"] = *fOpt.maxTokens
+ }
+ options["temperature"] = *fOpt.temperature
+ if fOpt.seed != nil && *fOpt.seed > 0 {
+ options["seed"] = *fOpt.seed
+ }
+
+ var keepAliveDuration *api.Duration
+ if *fOpt.keepAlive > 0 {
+ duration := api.Duration{Duration: time.Duration(*fOpt.keepAlive * float64(time.Second))}
+ keepAliveDuration = &duration
+ }
+
+ req := &api.ChatRequest{
+ Model: model,
+ Messages: []api.Message{
+ {
+ Role: "user",
+ Content: *fOpt.prompt,
+ },
+ },
+ Options: options,
+ KeepAlive: keepAliveDuration,
+ }
+
+ if imgData != nil {
+ req.Messages[0].Images = []api.ImageData{imgData}
+ }
+
+ var responseMetrics *api.Metrics
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*fOpt.timeout)*time.Second)
+ defer cancel()
+
+ err = client.Chat(ctx, req, func(resp api.ChatResponse) error {
+ if *fOpt.debug {
+ fmt.Fprintf(os.Stderr, "%s", cmp.Or(resp.Message.Thinking, resp.Message.Content))
+ }
+
+ if resp.Done {
+ responseMetrics = &resp.Metrics
+ }
+ return nil
+ })
+
+ if *fOpt.debug {
+ fmt.Fprintln(os.Stderr)
+ }
+
+ if err != nil {
+ if ctx.Err() == context.DeadlineExceeded {
+ fmt.Fprintf(os.Stderr, "ERROR: Chat request timed out with model '%s' after %vs\n", model, 1)
+ continue
+ }
+ fmt.Fprintf(os.Stderr, "ERROR: Couldn't chat with model '%s': %v\n", model, err)
+ continue
+ }
+
+ if responseMetrics == nil {
+ fmt.Fprintf(os.Stderr, "ERROR: No metrics received for model '%s'\n", model)
+ continue
+ }
+
+ metrics := []Metrics{
+ {
+ Model: model,
+ Step: "prefill",
+ Count: responseMetrics.PromptEvalCount,
+ Duration: responseMetrics.PromptEvalDuration,
+ },
+ {
+ Model: model,
+ Step: "generate",
+ Count: responseMetrics.EvalCount,
+ Duration: responseMetrics.EvalDuration,
+ },
+ {
+ Model: model,
+ Step: "load",
+ Count: 1,
+ Duration: responseMetrics.LoadDuration,
+ },
+ {
+ Model: model,
+ Step: "total",
+ Count: 1,
+ Duration: responseMetrics.TotalDuration,
+ },
+ }
+
+ OutputMetrics(out, *fOpt.format, metrics, *fOpt.verbose)
+
+ if *fOpt.keepAlive > 0 {
+ time.Sleep(time.Duration(*fOpt.keepAlive*float64(time.Second)) + 200*time.Millisecond)
+ }
+ }
+ }
+
+ return nil
+}
+
+func readImage(filePath string) (api.ImageData, error) {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ data, err := io.ReadAll(file)
+ if err != nil {
+ return nil, err
+ }
+
+ return api.ImageData(data), nil
+}
+
+func main() {
+ fOpt := flagOptions{
+ models: flag.String("model", "", "Model to benchmark"),
+ epochs: flag.Int("epochs", 6, "Number of epochs (iterations) per model"),
+ maxTokens: flag.Int("max-tokens", 200, "Maximum tokens for model response"),
+ temperature: flag.Float64("temperature", 0, "Temperature parameter"),
+ seed: flag.Int("seed", 0, "Random seed"),
+ timeout: flag.Int("timeout", 60*5, "Timeout in seconds (default 300s)"),
+ prompt: flag.String("p", DefaultPrompt, "Prompt to use"),
+ imageFile: flag.String("image", "", "Filename for an image to include"),
+ keepAlive: flag.Float64("k", 0, "Keep alive duration in seconds"),
+ format: flag.String("format", "markdown", "Output format [benchstat|csv] (default benchstat)"),
+ outputFile: flag.String("output", "", "Output file for results (stdout if empty)"),
+ verbose: flag.Bool("v", false, "Show system information"),
+ debug: flag.Bool("debug", false, "Show debug information"),
+ }
+
+ flag.Usage = func() {
+ fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS]\n\n", os.Args[0])
+ fmt.Fprintf(os.Stderr, "Description:\n")
+ fmt.Fprintf(os.Stderr, " Model benchmarking tool with configurable parameters\n\n")
+ fmt.Fprintf(os.Stderr, "Options:\n")
+ flag.PrintDefaults()
+ fmt.Fprintf(os.Stderr, "\nExamples:\n")
+ fmt.Fprintf(os.Stderr, " bench -model gpt-oss:20b -epochs 3 -temperature 0.7\n")
+ }
+ flag.Parse()
+
+ if !slices.Contains([]string{"markdown", "benchstat", "csv"}, *fOpt.format) {
+ fmt.Fprintf(os.Stderr, "ERROR: Unknown format '%s'\n", *fOpt.format)
+ os.Exit(1)
+ }
+
+ if len(*fOpt.models) == 0 {
+ fmt.Fprintf(os.Stderr, "ERROR: No model(s) specified to benchmark.\n")
+ flag.Usage()
+ return
+ }
+
+ BenchmarkChat(fOpt)
+}
diff --git a/cmd/bench/bench_test.go b/cmd/bench/bench_test.go
new file mode 100644
index 00000000000..bcd282d79f4
--- /dev/null
+++ b/cmd/bench/bench_test.go
@@ -0,0 +1,463 @@
+package main
+
+import (
+ "bytes"
+ "crypto/rand"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/ollama/ollama/api"
+)
+
+func createTestFlagOptions() flagOptions {
+ models := "test-model"
+ format := "benchstat"
+ epochs := 1
+ maxTokens := 100
+ temperature := 0.7
+ seed := 42
+ timeout := 30
+ prompt := "test prompt"
+ imageFile := ""
+ keepAlive := 5.0
+ verbose := false
+ debug := false
+
+ return flagOptions{
+ models: &models,
+ format: &format,
+ epochs: &epochs,
+ maxTokens: &maxTokens,
+ temperature: &temperature,
+ seed: &seed,
+ timeout: &timeout,
+ prompt: &prompt,
+ imageFile: &imageFile,
+ keepAlive: &keepAlive,
+ verbose: &verbose,
+ debug: &debug,
+ }
+}
+
+func captureOutput(f func()) string {
+ oldStdout := os.Stdout
+ oldStderr := os.Stderr
+ defer func() {
+ os.Stdout = oldStdout
+ os.Stderr = oldStderr
+ }()
+
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+ os.Stderr = w
+
+ f()
+
+ w.Close()
+ var buf bytes.Buffer
+ io.Copy(&buf, r)
+ return buf.String()
+}
+
+func createMockOllamaServer(t *testing.T, responses []api.ChatResponse) *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/api/chat" {
+ t.Errorf("Expected path /api/chat, got %s", r.URL.Path)
+ http.Error(w, "Not found", http.StatusNotFound)
+ return
+ }
+
+ if r.Method != "POST" {
+ t.Errorf("Expected POST method, got %s", r.Method)
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ for _, resp := range responses {
+ jsonData, err := json.Marshal(resp)
+ if err != nil {
+ t.Errorf("Failed to marshal response: %v", err)
+ return
+ }
+ w.Write(jsonData)
+ w.Write([]byte("\n"))
+ if f, ok := w.(http.Flusher); ok {
+ f.Flush()
+ }
+ time.Sleep(10 * time.Millisecond) // Simulate some delay
+ }
+ }))
+}
+
+func TestBenchmarkChat_Success(t *testing.T) {
+ fOpt := createTestFlagOptions()
+
+ mockResponses := []api.ChatResponse{
+ {
+ Model: "test-model",
+ Message: api.Message{
+ Role: "assistant",
+ Content: "test response part 1",
+ },
+ Done: false,
+ },
+ {
+ Model: "test-model",
+ Message: api.Message{
+ Role: "assistant",
+ Content: "test response part 2",
+ },
+ Done: true,
+ Metrics: api.Metrics{
+ PromptEvalCount: 10,
+ PromptEvalDuration: 100 * time.Millisecond,
+ EvalCount: 50,
+ EvalDuration: 500 * time.Millisecond,
+ TotalDuration: 600 * time.Millisecond,
+ LoadDuration: 50 * time.Millisecond,
+ },
+ },
+ }
+
+ server := createMockOllamaServer(t, mockResponses)
+ defer server.Close()
+
+ t.Setenv("OLLAMA_HOST", server.URL)
+
+ output := captureOutput(func() {
+ err := BenchmarkChat(fOpt)
+ if err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+ })
+
+ if !strings.Contains(output, "BenchmarkModel/name=test-model/step=prefill") {
+ t.Errorf("Expected output to contain prefill metrics, got: %s", output)
+ }
+ if !strings.Contains(output, "BenchmarkModel/name=test-model/step=generate") {
+ t.Errorf("Expected output to contain generate metrics, got: %s", output)
+ }
+ if !strings.Contains(output, "ns/token") {
+ t.Errorf("Expected output to contain ns/token metric, got: %s", output)
+ }
+}
+
+func TestBenchmarkChat_ServerError(t *testing.T) {
+ fOpt := createTestFlagOptions()
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "Internal server error", http.StatusInternalServerError)
+ }))
+ defer server.Close()
+
+ t.Setenv("OLLAMA_HOST", server.URL)
+
+ output := captureOutput(func() {
+ err := BenchmarkChat(fOpt)
+ if err != nil {
+ t.Errorf("Expected error to be handled internally, got returned error: %v", err)
+ }
+ })
+
+ if !strings.Contains(output, "ERROR: Couldn't chat with model") {
+ t.Errorf("Expected error message about chat failure, got: %s", output)
+ }
+}
+
+func TestBenchmarkChat_Timeout(t *testing.T) {
+ fOpt := createTestFlagOptions()
+ shortTimeout := 1 // Very short timeout
+ fOpt.timeout = &shortTimeout
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Simulate a long delay that will cause timeout
+ time.Sleep(2 * time.Second)
+
+ w.Header().Set("Content-Type", "application/json")
+ response := api.ChatResponse{
+ Model: "test-model",
+ Message: api.Message{
+ Role: "assistant",
+ Content: "test response",
+ },
+ Done: true,
+ Metrics: api.Metrics{
+ PromptEvalCount: 10,
+ PromptEvalDuration: 100 * time.Millisecond,
+ EvalCount: 50,
+ EvalDuration: 500 * time.Millisecond,
+ TotalDuration: 600 * time.Millisecond,
+ LoadDuration: 50 * time.Millisecond,
+ },
+ }
+ jsonData, _ := json.Marshal(response)
+ w.Write(jsonData)
+ }))
+ defer server.Close()
+
+ t.Setenv("OLLAMA_HOST", server.URL)
+
+ output := captureOutput(func() {
+ err := BenchmarkChat(fOpt)
+ if err != nil {
+ t.Errorf("Expected timeout to be handled internally, got returned error: %v", err)
+ }
+ })
+
+ if !strings.Contains(output, "ERROR: Chat request timed out") {
+ t.Errorf("Expected timeout error message, got: %s", output)
+ }
+}
+
+func TestBenchmarkChat_NoMetrics(t *testing.T) {
+ fOpt := createTestFlagOptions()
+
+ mockResponses := []api.ChatResponse{
+ {
+ Model: "test-model",
+ Message: api.Message{
+ Role: "assistant",
+ Content: "test response",
+ },
+ Done: false, // Never sends Done=true
+ },
+ }
+
+ server := createMockOllamaServer(t, mockResponses)
+ defer server.Close()
+
+ t.Setenv("OLLAMA_HOST", server.URL)
+
+ output := captureOutput(func() {
+ err := BenchmarkChat(fOpt)
+ if err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+ })
+
+ if !strings.Contains(output, "ERROR: No metrics received") {
+ t.Errorf("Expected no metrics error message, got: %s", output)
+ }
+}
+
+func TestBenchmarkChat_MultipleModels(t *testing.T) {
+ fOpt := createTestFlagOptions()
+ models := "model1,model2"
+ epochs := 2
+ fOpt.models = &models
+ fOpt.epochs = &epochs
+
+ callCount := 0
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ callCount++
+
+ w.Header().Set("Content-Type", "application/json")
+
+ var req api.ChatRequest
+ body, _ := io.ReadAll(r.Body)
+ json.Unmarshal(body, &req)
+
+ response := api.ChatResponse{
+ Model: req.Model,
+ Message: api.Message{
+ Role: "assistant",
+ Content: "test response for " + req.Model,
+ },
+ Done: true,
+ Metrics: api.Metrics{
+ PromptEvalCount: 10,
+ PromptEvalDuration: 100 * time.Millisecond,
+ EvalCount: 50,
+ EvalDuration: 500 * time.Millisecond,
+ TotalDuration: 600 * time.Millisecond,
+ LoadDuration: 50 * time.Millisecond,
+ },
+ }
+ jsonData, _ := json.Marshal(response)
+ w.Write(jsonData)
+ }))
+ defer server.Close()
+
+ t.Setenv("OLLAMA_HOST", server.URL)
+
+ output := captureOutput(func() {
+ err := BenchmarkChat(fOpt)
+ if err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+ })
+
+ // Should be called 4 times (2 models × 2 epochs)
+ if callCount != 4 {
+ t.Errorf("Expected 4 API calls, got %d", callCount)
+ }
+
+ if !strings.Contains(output, "BenchmarkModel/name=model1") || !strings.Contains(output, "BenchmarkModel/name=model2") {
+ t.Errorf("Expected output for both models, got: %s", output)
+ }
+}
+
+func TestBenchmarkChat_WithImage(t *testing.T) {
+ fOpt := createTestFlagOptions()
+
+ tmpfile, err := os.CreateTemp(t.TempDir(), "testimage")
+ if err != nil {
+ t.Fatalf("Failed to create temp file: %v", err)
+ }
+ defer os.Remove(tmpfile.Name())
+
+ content := []byte("fake image data")
+ if _, err := tmpfile.Write(content); err != nil {
+ t.Fatalf("Failed to write to temp file: %v", err)
+ }
+ tmpfile.Close()
+
+ tmpfileName := tmpfile.Name()
+ fOpt.imageFile = &tmpfileName
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Verify the request contains image data
+ var req api.ChatRequest
+ body, _ := io.ReadAll(r.Body)
+ json.Unmarshal(body, &req)
+
+ if len(req.Messages) == 0 || len(req.Messages[0].Images) == 0 {
+ t.Error("Expected request to contain images")
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ response := api.ChatResponse{
+ Model: "test-model",
+ Message: api.Message{
+ Role: "assistant",
+ Content: "test response with image",
+ },
+ Done: true,
+ Metrics: api.Metrics{
+ PromptEvalCount: 10,
+ PromptEvalDuration: 100 * time.Millisecond,
+ EvalCount: 50,
+ EvalDuration: 500 * time.Millisecond,
+ TotalDuration: 600 * time.Millisecond,
+ LoadDuration: 50 * time.Millisecond,
+ },
+ }
+ jsonData, _ := json.Marshal(response)
+ w.Write(jsonData)
+ }))
+ defer server.Close()
+
+ t.Setenv("OLLAMA_HOST", server.URL)
+
+ output := captureOutput(func() {
+ err := BenchmarkChat(fOpt)
+ if err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+ })
+
+ if !strings.Contains(output, "BenchmarkModel/name=test-model") {
+ t.Errorf("Expected benchmark output, got: %s", output)
+ }
+}
+
+func TestBenchmarkChat_ImageError(t *testing.T) {
+ randFileName := func() string {
+ const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
+ const length = 8
+
+ result := make([]byte, length)
+ rand.Read(result) // Fill with random bytes
+
+ for i := range result {
+ result[i] = charset[result[i]%byte(len(charset))]
+ }
+
+ return string(result) + ".txt"
+ }
+
+ fOpt := createTestFlagOptions()
+ imageFile := randFileName()
+ fOpt.imageFile = &imageFile
+
+ output := captureOutput(func() {
+ err := BenchmarkChat(fOpt)
+ if err == nil {
+ t.Error("Expected error from image reading, got nil")
+ }
+ })
+
+ if !strings.Contains(output, "ERROR: Couldn't read image") {
+ t.Errorf("Expected image read error message, got: %s", output)
+ }
+}
+
+func TestReadImage_Success(t *testing.T) {
+ tmpfile, err := os.CreateTemp(t.TempDir(), "testimage")
+ if err != nil {
+ t.Fatalf("Failed to create temp file: %v", err)
+ }
+ defer os.Remove(tmpfile.Name())
+
+ content := []byte("fake image data")
+ if _, err := tmpfile.Write(content); err != nil {
+ t.Fatalf("Failed to write to temp file: %v", err)
+ }
+ tmpfile.Close()
+
+ imgData, err := readImage(tmpfile.Name())
+ if err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+
+ if imgData == nil {
+ t.Error("Expected image data, got nil")
+ }
+
+ expected := api.ImageData(content)
+ if string(imgData) != string(expected) {
+ t.Errorf("Expected image data %v, got %v", expected, imgData)
+ }
+}
+
+func TestReadImage_FileNotFound(t *testing.T) {
+ imgData, err := readImage("nonexistentfile.jpg")
+ if err == nil {
+ t.Error("Expected error for non-existent file, got nil")
+ }
+ if imgData != nil {
+ t.Error("Expected nil image data for non-existent file")
+ }
+}
+
+func TestOptionsMapCreation(t *testing.T) {
+ fOpt := createTestFlagOptions()
+
+ options := make(map[string]interface{})
+ if *fOpt.maxTokens > 0 {
+ options["num_predict"] = *fOpt.maxTokens
+ }
+ options["temperature"] = *fOpt.temperature
+ if fOpt.seed != nil && *fOpt.seed > 0 {
+ options["seed"] = *fOpt.seed
+ }
+
+ if options["num_predict"] != *fOpt.maxTokens {
+ t.Errorf("Expected num_predict %d, got %v", *fOpt.maxTokens, options["num_predict"])
+ }
+ if options["temperature"] != *fOpt.temperature {
+ t.Errorf("Expected temperature %f, got %v", *fOpt.temperature, options["temperature"])
+ }
+ if options["seed"] != *fOpt.seed {
+ t.Errorf("Expected seed %d, got %v", *fOpt.seed, options["seed"])
+ }
+}
diff --git a/cmd/cmd.go b/cmd/cmd.go
index b18d9043276..9306fb2cadc 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -45,6 +45,7 @@ import (
"github.com/ollama/ollama/types/model"
"github.com/ollama/ollama/types/syncmap"
"github.com/ollama/ollama/version"
+ xcmd "github.com/ollama/ollama/x/cmd"
)
const ConnectInstructions = "To sign in, navigate to:\n %s\n\n"
@@ -517,6 +518,10 @@ func RunHandler(cmd *cobra.Command, args []string) error {
return generateEmbedding(cmd, name, opts.Prompt, opts.KeepAlive, truncate, dimensions)
}
+ // Check for experimental flag
+ isExperimental, _ := cmd.Flags().GetBool("experimental")
+ yoloMode, _ := cmd.Flags().GetBool("yolo")
+
if interactive {
if err := loadOrUnloadModel(cmd, &opts); err != nil {
var sErr api.AuthorizationError
@@ -543,6 +548,11 @@ func RunHandler(cmd *cobra.Command, args []string) error {
}
}
+ // Use experimental agent loop with tools
+ if isExperimental {
+ return xcmd.GenerateInteractive(cmd, opts.Model, opts.WordWrap, opts.Options, opts.Think, opts.HideThinking, opts.KeepAlive, yoloMode)
+ }
+
return generateInteractive(cmd, opts)
}
return generate(cmd, opts)
@@ -943,6 +953,9 @@ func showInfo(resp *api.ShowResponse, verbose bool, w io.Writer) error {
rows = append(rows, []string{"", "parameters", resp.Details.ParameterSize})
}
rows = append(rows, []string{"", "quantization", resp.Details.QuantizationLevel})
+ if resp.Requires != "" {
+ rows = append(rows, []string{"", "requires", resp.Requires})
+ }
return
})
@@ -1430,7 +1443,7 @@ func chat(cmd *cobra.Command, opts runOptions) (*api.Message, error) {
latest.Summary()
}
- return &api.Message{Role: role, Content: fullResponse.String()}, nil
+ return &api.Message{Role: role, Thinking: thinkingContent.String(), Content: fullResponse.String()}, nil
}
func generate(cmd *cobra.Command, opts runOptions) error {
@@ -1751,6 +1764,8 @@ func NewCLI() *cobra.Command {
runCmd.Flags().Bool("hidethinking", false, "Hide thinking output (if provided)")
runCmd.Flags().Bool("truncate", false, "For embedding models: truncate inputs exceeding context length (default: true). Set --truncate=false to error instead")
runCmd.Flags().Int("dimensions", 0, "Truncate output embeddings to specified dimension (embedding models only)")
+ runCmd.Flags().Bool("experimental", false, "Enable experimental agent loop with tools")
+ runCmd.Flags().BoolP("yolo", "y", false, "Skip all tool approval prompts (use with caution)")
stopCmd := &cobra.Command{
Use: "stop MODEL",
diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go
index 1c9d1994253..7dc3d0093ae 100644
--- a/cmd/cmd_test.go
+++ b/cmd/cmd_test.go
@@ -291,6 +291,31 @@ Weigh anchor!
t.Errorf("unexpected output (-want +got):\n%s", diff)
}
})
+
+ t.Run("min version", func(t *testing.T) {
+ var b bytes.Buffer
+ if err := showInfo(&api.ShowResponse{
+ Details: api.ModelDetails{
+ Family: "test",
+ ParameterSize: "7B",
+ QuantizationLevel: "FP16",
+ },
+ Requires: "0.14.0",
+ }, false, &b); err != nil {
+ t.Fatal(err)
+ }
+
+ expect := ` Model
+ architecture test
+ parameters 7B
+ quantization FP16
+ requires 0.14.0
+
+`
+ if diff := cmp.Diff(expect, b.String()); diff != "" {
+ t.Errorf("unexpected output (-want +got):\n%s", diff)
+ }
+ })
}
func TestDeleteHandler(t *testing.T) {
diff --git a/cmd/interactive.go b/cmd/interactive.go
index cf0aced1483..9c4e32a2e75 100644
--- a/cmd/interactive.go
+++ b/cmd/interactive.go
@@ -40,6 +40,7 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
fmt.Fprintln(os.Stderr, " /bye Exit")
fmt.Fprintln(os.Stderr, " /?, /help Help for a command")
fmt.Fprintln(os.Stderr, " /? shortcuts Help for keyboard shortcuts")
+
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Use \"\"\" to begin a multi-line message.")
diff --git a/convert/convert.go b/convert/convert.go
index 3e98eee1ac7..a6d286683d4 100644
--- a/convert/convert.go
+++ b/convert/convert.go
@@ -182,6 +182,8 @@ func ConvertModel(fsys fs.FS, f *os.File) error {
conv = &llama4Model{}
case "Mistral3ForConditionalGeneration":
conv = &mistral3Model{}
+ case "Ministral3ForCausalLM":
+ conv = &mistral3CausalModel{}
case "MixtralForCausalLM":
conv = &mixtralModel{}
case "GemmaForCausalLM":
@@ -200,12 +202,20 @@ func ConvertModel(fsys fs.FS, f *os.File) error {
conv = &qwen25VLModel{}
case "Qwen3VLForConditionalGeneration", "Qwen3VLMoeForConditionalGeneration":
conv = &qwen3VLModel{}
+ case "Olmo3ForCausalLM":
+ conv = &olmoModel{}
case "BertModel":
conv = &bertModel{}
+ case "NomicBertModel", "NomicBertMoEModel":
+ conv = &nomicbertModel{}
case "CohereForCausalLM":
conv = &commandrModel{}
case "GptOssForCausalLM":
conv = &gptossModel{}
+ case "DeepseekOCRForCausalLM":
+ conv = &deepseekocr{}
+ case "DeepseekV3ForCausalLM":
+ conv = &deepseek2Model{}
default:
return fmt.Errorf("unsupported architecture %q", p.Architectures[0])
}
diff --git a/convert/convert_deepseek2.go b/convert/convert_deepseek2.go
new file mode 100644
index 00000000000..aa6203277a7
--- /dev/null
+++ b/convert/convert_deepseek2.go
@@ -0,0 +1,173 @@
+package convert
+
+import (
+ "cmp"
+ "fmt"
+ "log/slog"
+ "regexp"
+ "strconv"
+
+ "github.com/ollama/ollama/fs/ggml"
+)
+
+type deepseek2Model struct {
+ ModelParameters // architectures, vocab_size
+ MaxPositionEmbeddings uint32 `json:"max_position_embeddings"`
+ HiddenSize uint32 `json:"hidden_size"`
+ HiddenLayers uint32 `json:"num_hidden_layers"`
+ IntermediateSize uint32 `json:"intermediate_size"`
+ NumAttentionHeads uint32 `json:"num_attention_heads"`
+ NumKeyValueHeads uint32 `json:"num_key_value_heads"`
+ RMSNormEPS float32 `json:"rms_norm_eps"`
+
+ RopeTheta float32 `json:"rope_theta"`
+ QKNopeHeadDim uint32 `json:"qk_nope_head_dim"`
+ QKRopeHeadDim uint32 `json:"qk_rope_head_dim"`
+ KVLoraRank uint32 `json:"kv_lora_rank"`
+ QLoraRank uint32 `json:"q_lora_rank"`
+ VHeadDim uint32 `json:"v_head_dim"`
+
+ ExpertCount uint32 `json:"n_routed_experts"`
+ ExpertSharedCount uint32 `json:"n_shared_experts"`
+ ExpertIntermediateSize uint32 `json:"moe_intermediate_size"`
+ ExpertUsedCount uint32 `json:"num_experts_per_tok"`
+ ExpertWeightsNorm bool `json:"norm_topk_prob"`
+ ExpertWeightsScale float32 `json:"routed_scaling_factor"`
+
+ ScoringFunc string `json:"scoring_func"`
+ LeadingDenseBlockCount uint32 `json:"first_k_dense_replace"`
+
+ RopeScaling struct {
+ Factor float32 `json:"factor"`
+ OriginalMaxPositionEmbeddings uint32 `json:"original_max_position_embeddings"`
+ Type string `json:"type"`
+ MScaleAllDim float32 `json:"mscale_all_dim"`
+ } `json:"rope_scaling"`
+
+ Architecture string
+}
+
+func (p *deepseek2Model) KV(t *Tokenizer) ggml.KV {
+ kv := p.ModelParameters.KV(t)
+ kv["general.architecture"] = "deepseek2"
+ kv["general.type"] = "model"
+ kv["deepseek2.block_count"] = p.HiddenLayers
+
+ numHeads := p.NumAttentionHeads
+ numKVHeads := p.NumKeyValueHeads
+
+ kv["deepseek2.attention.head_count"] = numHeads
+ kv["deepseek2.attention.head_count_kv"] = numKVHeads
+ kv["deepseek2.attention.key_length"] = p.QKNopeHeadDim + p.QKRopeHeadDim
+ kv["deepseek2.attention.kv_lora_rank"] = p.KVLoraRank
+ kv["deepseek2.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
+ kv["deepseek2.attention.q_lora_rank"] = p.QLoraRank
+ kv["deepseek2.attention.value_length"] = p.VHeadDim
+ kv["deepseek2.context_length"] = p.MaxPositionEmbeddings
+ kv["deepseek2.embedding_length"] = p.HiddenSize
+ kv["deepseek2.expert_count"] = p.ExpertCount
+ kv["deepseek2.expert_feed_forward_length"] = p.ExpertIntermediateSize
+ kv["deepseek2.expert_shared_count"] = p.ExpertSharedCount
+
+ var scoringFunc uint32
+ switch p.ScoringFunc {
+ case "softmax":
+ // not currently supported in the model, but needed for Deepseek-OCR
+ scoringFunc = 1
+ case "sigmoid":
+ scoringFunc = 2
+ }
+ kv["deepseek2.expert_gating_func"] = scoringFunc
+ kv["deepseek2.expert_used_count"] = p.ExpertUsedCount
+ kv["deepseek2.expert_weights_norm"] = p.ExpertWeightsNorm
+ kv["deepseek2.expert_weights_scale"] = p.ExpertWeightsScale
+ kv["deepseek2.feed_forward_length"] = p.IntermediateSize
+ kv["deepseek2.leading_dense_block_count"] = p.LeadingDenseBlockCount
+
+ kv["deepseek2.rope.dimension_count"] = p.QKRopeHeadDim
+ kv["deepseek2.rope.freq_base"] = cmp.Or(p.RopeTheta, 10000.0)
+ kv["deepseek2.rope.scaling.factor"] = p.RopeScaling.Factor
+ kv["deepseek2.rope.scaling.original_context_length"] = p.RopeScaling.OriginalMaxPositionEmbeddings
+ kv["deepseek2.rope.scaling.type"] = p.RopeScaling.Type
+ kv["deepseek2.rope.scaling.yarn_log_multiplier"] = 0.1 * p.RopeScaling.MScaleAllDim
+
+ kv["tokenizer.ggml.pre"] = "deepseek-v3"
+
+ return kv
+}
+
+func (p *deepseek2Model) Replacements() []string {
+ return []string{
+ "lm_head", "output",
+ "model.embed_tokens", "token_embd",
+ "model.norm", "output_norm",
+ "language_model.", "",
+ "model.layers", "blk",
+ "input_layernorm", "attn_norm",
+ "self_attn.kv_a_proj_with_mqa", "attn_kv_a_mqa",
+ "self_attn.kv_a_layernorm", "attn_kv_a_norm",
+ "self_attn.kv_b_proj", "attn_kv_b",
+ "self_attn.q_a_proj", "attn_q_a",
+ "self_attn.q_a_layernorm", "attn_q_a_norm",
+ "self_attn.q_b_proj", "attn_q_b",
+ "self_attn.o_proj", "attn_output",
+ "post_attention_layernorm", "ffn_norm",
+ "mlp.shared_experts.down_proj", "ffn_down_shexp",
+ "mlp.shared_experts.gate_proj", "ffn_gate_shexp",
+ "mlp.shared_experts.up_proj", "ffn_up_shexp",
+ "mlp.gate_proj", "ffn_gate",
+ "mlp.down_proj", "ffn_down",
+ "mlp.up_proj", "ffn_up",
+ "mlp.gate.e_score_correction_bias", "exp_probs_b.bias",
+ "mlp.gate", "ffn_gate_inp",
+ }
+}
+
+func (p *deepseek2Model) Tensors(s []Tensor) (out []*ggml.Tensor) {
+ merges := make([]merge, p.HiddenLayers*3)
+ for i := range p.HiddenLayers {
+ merges[i*3+0] = merge{
+ fmt.Sprintf("blk.%d.mlp.experts.*.gate_proj.weight", i),
+ fmt.Sprintf("blk.%d.ffn_gate_exps.weight", i),
+ }
+ merges[i*3+1] = merge{
+ fmt.Sprintf("blk.%d.mlp.experts.*.up_proj.weight", i),
+ fmt.Sprintf("blk.%d.ffn_up_exps.weight", i),
+ }
+ merges[i*3+2] = merge{
+ fmt.Sprintf("blk.%d.mlp.experts.*.down_proj.weight", i),
+ fmt.Sprintf("blk.%d.ffn_down_exps.weight", i),
+ }
+ }
+
+ skipLayer := func(n string, minValue uint32) bool {
+ re := regexp.MustCompile(`^blk\.(\d+)`)
+ matches := re.FindStringSubmatch(n)
+ if matches == nil {
+ return false
+ }
+
+ blkNum, err := strconv.Atoi(matches[1])
+ if err != nil {
+ return false
+ }
+
+ return uint32(blkNum) >= minValue
+ }
+
+ out, s = mergeTensors(s, merges...)
+ for _, t := range s {
+ // skip any additional layers (such as the Multi-Token Prediction layer)
+ if skipLayer(t.Name(), p.HiddenLayers) {
+ slog.Debug("skipping layer", "name", t.Name())
+ continue
+ }
+ out = append(out, &ggml.Tensor{
+ Name: t.Name(),
+ Kind: t.Kind(),
+ Shape: t.Shape(),
+ WriterTo: t,
+ })
+ }
+ return out
+}
diff --git a/convert/convert_deepseekocr.go b/convert/convert_deepseekocr.go
new file mode 100644
index 00000000000..cf1dfa0c43c
--- /dev/null
+++ b/convert/convert_deepseekocr.go
@@ -0,0 +1,136 @@
+package convert
+
+import (
+ "fmt"
+
+ "github.com/ollama/ollama/fs/ggml"
+)
+
+type deepseekocr struct {
+ ModelParameters
+ LanguageConfig struct {
+ MaxPositionEmbeddings uint32 `json:"max_position_embeddings"`
+ HiddenSize uint32 `json:"hidden_size"`
+ HiddenLayers uint32 `json:"num_hidden_layers"`
+ IntermediateSize uint32 `json:"intermediate_size"`
+ NumAttentionHeads uint32 `json:"num_attention_heads"`
+ NumKeyValueHeads uint32 `json:"num_key_value_heads"`
+ NumRoutedExperts uint32 `json:"n_routed_experts"`
+ NumSharedExperts uint32 `json:"n_shared_experts"`
+ NumExpertsPerToken uint32 `json:"num_experts_per_tok"`
+ FirstKDenseReplace uint32 `json:"first_k_dense_replace"`
+ } `json:"language_config"`
+
+ VisionConfig struct {
+ ImageSize uint32 `json:"image_size"`
+ Width struct {
+ Vision struct {
+ Heads uint32 `json:"heads"`
+ ImageSize uint32 `json:"image_size"`
+ Layers uint32 `json:"layers"`
+ PatchSize uint32 `json:"patch_size"`
+ Width uint32 `json:"width"`
+ } `json:"clip-l-14-224"`
+ Sam struct {
+ GlobalAttentionIndexes []int32 `json:"global_attn_indexes"`
+ Heads uint32 `json:"heads"`
+ Layers uint32 `json:"layers"`
+ Width uint32 `json:"width"`
+ } `json:"sam_vit_b"`
+ }
+ } `json:"vision_config"`
+}
+
+func (m *deepseekocr) KV(t *Tokenizer) ggml.KV {
+ kv := m.ModelParameters.KV(t)
+ kv["general.architecture"] = "deepseekocr"
+ kv["block_count"] = m.LanguageConfig.HiddenLayers
+ kv["context_length"] = m.LanguageConfig.MaxPositionEmbeddings
+ kv["embedding_length"] = m.LanguageConfig.HiddenSize
+ kv["feed_forward_length"] = m.LanguageConfig.IntermediateSize
+ kv["attention.head_count"] = m.LanguageConfig.NumAttentionHeads
+ kv["attention.head_count_kv"] = m.LanguageConfig.NumKeyValueHeads
+ kv["expert_count"] = m.LanguageConfig.NumRoutedExperts
+ kv["expert_used_count"] = m.LanguageConfig.NumExpertsPerToken
+ kv["leading_dense_block_count"] = m.LanguageConfig.FirstKDenseReplace
+
+ kv["vision.block_count"] = m.VisionConfig.Width.Vision.Layers
+ kv["vision.embedding_length"] = m.VisionConfig.Width.Vision.Width
+ kv["vision.head_count"] = m.VisionConfig.Width.Vision.Heads
+ kv["vision.image_size"] = m.VisionConfig.Width.Vision.ImageSize
+ kv["vision.patch_size"] = m.VisionConfig.Width.Vision.PatchSize
+
+ kv["sam.block_count"] = m.VisionConfig.Width.Sam.Layers
+ kv["sam.embedding_length"] = m.VisionConfig.Width.Sam.Width
+ kv["sam.head_count"] = m.VisionConfig.Width.Sam.Heads
+ kv["sam.global_attention_indexes"] = m.VisionConfig.Width.Sam.GlobalAttentionIndexes
+ return kv
+}
+
+func (m *deepseekocr) Tensors(s []Tensor) (out []*ggml.Tensor) {
+ merges := make([]merge, m.LanguageConfig.HiddenLayers*3)
+ for i := range m.LanguageConfig.HiddenLayers {
+ merges[i*3+0] = merge{
+ fmt.Sprintf("blk.%d.mlp.experts.*.gate_proj.weight", i),
+ fmt.Sprintf("blk.%d.ffn_gate_exps.weight", i),
+ }
+ merges[i*3+1] = merge{
+ fmt.Sprintf("blk.%d.mlp.experts.*.up_proj.weight", i),
+ fmt.Sprintf("blk.%d.ffn_up_exps.weight", i),
+ }
+ merges[i*3+2] = merge{
+ fmt.Sprintf("blk.%d.mlp.experts.*.down_proj.weight", i),
+ fmt.Sprintf("blk.%d.ffn_down_exps.weight", i),
+ }
+ }
+
+ out, s = mergeTensors(s, merges...)
+ for _, t := range s {
+ out = append(out, &ggml.Tensor{
+ Name: t.Name(),
+ Kind: t.Kind(),
+ Shape: t.Shape(),
+ WriterTo: t,
+ })
+ }
+ return out
+}
+
+func (m *deepseekocr) Replacements() []string {
+ return []string{
+ "model.embed_tokens", "token_embd",
+ "model.layers", "blk",
+ "input_layernorm", "attn_norm",
+ "self_attn.q_proj", "attn_q",
+ "self_attn.k_proj", "attn_k",
+ "self_attn.v_proj", "attn_v",
+ "self_attn.o_proj", "attn_output",
+ "post_attention_layernorm", "ffn_norm",
+ "mlp.gate_proj", "ffn_gate",
+ "mlp.up_proj", "ffn_up",
+ "mlp.down_proj", "ffn_down",
+ "mlp.gate", "ffn_gate_inp",
+ "mlp.shared_experts.gate_proj", "ffn_gate_shexp",
+ "mlp.shared_experts.up_proj", "ffn_up_shexp",
+ "mlp.shared_experts.down_proj", "ffn_down_shexp",
+ "model.norm", "output_norm",
+ "lm_head", "output",
+
+ "model.vision_model", "v",
+ "embeddings.patch_embedding", "patch_embd",
+ "embeddings.class_embedding", "class_embd",
+ "embeddings.position_embedding", "position_embd",
+ "transformer.layers", "blk",
+
+ "model.projector", "mm",
+ "model.image_newline", "mm.image_newline",
+ //nolint:misspell // this misspelling is upstream. fixing it breaks the model
+ "model.view_seperator", "mm.view_seperator",
+
+ "model.sam_model.patch_embed.proj", "s.patch_embd",
+ "model.sam_model.pos_embed", "s.position_embd",
+ "model.sam_model.blocks", "s.blk",
+ "model.sam_model.neck", "s.neck",
+ "model.sam_model.net_", "s.net_",
+ }
+}
diff --git a/convert/convert_gemma3.go b/convert/convert_gemma3.go
index 27b99f575a2..5e6e6904c23 100644
--- a/convert/convert_gemma3.go
+++ b/convert/convert_gemma3.go
@@ -2,6 +2,7 @@ package convert
import (
"cmp"
+ "slices"
"github.com/ollama/ollama/fs/ggml"
)
@@ -26,16 +27,26 @@ type gemma3Model struct {
NumChannels uint32 `json:"num_channels"` // num_channels 3
PatchSize uint32 `json:"patch_size"` // patch_size 14
} `json:"vision_config"`
- MaxPositionEmbeddings uint32 `json:"max_position_embeddings"`
- NumAttentionHeads uint32 `json:"num_attention_heads"`
- NumKeyValueHeads uint32 `json:"num_key_value_heads"`
- RMSNormEPS float32 `json:"rms_norm_eps"`
- HeadDim uint32 `json:"head_dim"`
- FinalLogitSoftcap float32 `json:"final_logit_softcapping"`
- RopeLocalTheta float32 `json:"rope_local_base_freq"`
- RopeGlobalTheta float32 `json:"rope_global_base_freq"`
- SlidingWindow uint32 `json:"sliding_window"`
- MultiModalTokensPerImage uint32 `json:"mm_tokens_per_image"`
+ MaxPositionEmbeddings uint32 `json:"max_position_embeddings"`
+ NumAttentionHeads uint32 `json:"num_attention_heads"`
+ NumKeyValueHeads uint32 `json:"num_key_value_heads"`
+ RMSNormEPS float32 `json:"rms_norm_eps"`
+ HeadDim uint32 `json:"head_dim"`
+ FinalLogitSoftcap float32 `json:"final_logit_softcapping"`
+ RopeLocalTheta float32 `json:"rope_local_base_freq"`
+ RopeTheta float32 `json:"rope_theta"`
+ SlidingWindow uint32 `json:"sliding_window"`
+ SlidingWindowPattern *uint32 `json:"sliding_window_pattern"`
+ LayerTypes []string `json:"layer_types"`
+ MultiModalTokensPerImage uint32 `json:"mm_tokens_per_image"`
+ RopeScaling *struct {
+ Type string `json:"rope_type"`
+ Factor float32 `json:"factor"`
+ OriginalMaxPositionEmbeddings uint32 `json:"original_max_position_embeddings"`
+ ExtrapolationFactor float32 `json:"extrapolation_factor"`
+ BetaFast float32 `json:"beta_fast"`
+ BetaSlow float32 `json:"beta_slow"`
+ } `json:"rope_scaling"`
}
const (
@@ -81,9 +92,38 @@ func (p *gemma3Model) KV(t *Tokenizer) ggml.KV {
kv["gemma3.attention.key_length"] = p.HeadDim
kv["gemma3.attention.value_length"] = p.HeadDim
kv["gemma3.attention.sliding_window"] = p.SlidingWindow
- kv["gemma3.final_logit_softcapping"] = cmp.Or(p.FinalLogitSoftcap, 30)
+
+ // The sliding window pattern is either provided as the sliding_window_pattern
+ // key (an int) or as the layer_types key (a list of strings).
+ if p.SlidingWindowPattern != nil || len(p.LayerTypes) > 0 {
+ kv["gemma3.attention.sliding_window_pattern"] = slices.Collect(func(yield func(bool) bool) {
+ for i := range numBlocks {
+ var isLocal bool
+ if len(p.LayerTypes) > 0 && int(i) < len(p.LayerTypes) {
+ isLocal = p.LayerTypes[i] == "sliding_attention"
+ } else if p.SlidingWindowPattern != nil && *p.SlidingWindowPattern > 0 {
+ isLocal = (i+1)%*p.SlidingWindowPattern != 0
+ }
+ if !yield(isLocal) {
+ break
+ }
+ }
+ })
+ }
+ if p.FinalLogitSoftcap > 0 {
+ kv["gemma3.final_logit_softcapping"] = p.FinalLogitSoftcap
+ }
kv["gemma3.rope.local.freq_base"] = cmp.Or(p.RopeLocalTheta, 10000.0)
- kv["gemma3.rope.global.freq_base"] = cmp.Or(p.RopeGlobalTheta, 1000000.0)
+ kv["gemma3.rope.freq_base"] = cmp.Or(p.RopeTheta, 1000000.0)
+ if p.RopeScaling != nil && p.RopeScaling.Type == "yarn" && p.RopeScaling.Factor > 0 {
+ kv["gemma3.rope.scaling.type"] = "yarn"
+ kv["gemma3.rope.scaling.factor"] = p.RopeScaling.Factor
+ kv["gemma3.rope.scaling.original_context_length"] = p.RopeScaling.OriginalMaxPositionEmbeddings
+ kv["gemma3.rope.scaling.extrapolation_factor"] = cmp.Or(p.RopeScaling.ExtrapolationFactor, float32(1.0))
+ kv["gemma3.rope.scaling.beta_fast"] = cmp.Or(p.RopeScaling.BetaFast, float32(64.0))
+ kv["gemma3.rope.scaling.beta_slow"] = cmp.Or(p.RopeScaling.BetaSlow, float32(1.0))
+ }
+
kv["gemma3.embedding_length"] = p.HiddenSize
kv["gemma3.feed_forward_length"] = p.IntermediateSize
default:
diff --git a/convert/convert_mistral.go b/convert/convert_mistral.go
index a6fd4c41a99..f11bd96440d 100644
--- a/convert/convert_mistral.go
+++ b/convert/convert_mistral.go
@@ -29,6 +29,17 @@ type mistral3Model struct {
SlidingWindow *uint32 `json:"sliding_window"`
HiddenAct string `json:"hidden_act"`
VocabSize uint32 `json:"vocab_size"`
+ RopeParameters struct {
+ BetaFast float32 `json:"beta_fast"`
+ BetaSlow float32 `json:"beta_slow"`
+ Factor float32 `json:"factor"`
+ Llama4ScalingBeta *float32 `json:"llama_4_scaling_beta"`
+ OrigMaxPositionEmbeddings uint32 `json:"original_max_position_embeddings"`
+ RopeType string `json:"rope_type"`
+ RopeTheta float32 `json:"rope_theta"`
+ Mscale *float32 `json:"mscale"`
+ MscaleAllDim *float32 `json:"mscale_all_dim"`
+ } `json:"rope_parameters"`
} `json:"text_config"`
VisionModel struct {
NumAttentionHeads uint32 `json:"num_attention_heads"`
@@ -41,6 +52,9 @@ type mistral3Model struct {
HeadDim uint32 `json:"head_dim"`
HiddenAct string `json:"hidden_act"`
RopeTheta float32 `json:"rope_theta"`
+ RopeParameters struct {
+ RopeTheta float32 `json:"rope_theta"`
+ } `json:"rope_parameters"`
} `json:"vision_config"`
MultiModalProjectorBias bool `json:"multimodal_projector_bias"`
ProjectorHiddenAct string `json:"projector_hidden_act"`
@@ -61,8 +75,25 @@ func (p *mistral3Model) KV(t *Tokenizer) ggml.KV {
kv["mistral3.attention.layer_norm_rms_epsilon"] = p.TextModel.RMSNormEPS
kv["mistral3.attention.key_length"] = p.TextModel.HeadDim
kv["mistral3.attention.value_length"] = p.TextModel.HeadDim
- kv["mistral3.rope.dimension_count"] = p.TextModel.HiddenSize / p.TextModel.NumHiddenLayers
- kv["mistral3.rope.freq_base"] = p.TextModel.RopeTheta
+ kv["mistral3.rope.dimension_count"] = cmp.Or(p.TextModel.HeadDim, p.TextModel.HiddenSize/p.TextModel.NumAttentionHeads)
+ kv["mistral3.rope.freq_base"] = cmp.Or(p.TextModel.RopeTheta, p.TextModel.RopeParameters.RopeTheta)
+ kv["mistral3.rope.scaling.factor"] = p.TextModel.RopeParameters.Factor
+ kv["mistral3.rope.scaling.type"] = p.TextModel.RopeParameters.RopeType
+ kv["mistral3.rope.scaling.beta_fast"] = p.TextModel.RopeParameters.BetaFast
+ kv["mistral3.rope.scaling.beta_slow"] = p.TextModel.RopeParameters.BetaSlow
+
+ if p.TextModel.RopeParameters.Mscale != nil {
+ kv["mistral3.rope.scaling.mscale"] = *p.TextModel.RopeParameters.Mscale
+ }
+ if p.TextModel.RopeParameters.MscaleAllDim != nil {
+ kv["mistral3.rope.scaling.mscale_all_dim"] = *p.TextModel.RopeParameters.MscaleAllDim
+ }
+ if p.TextModel.RopeParameters.OrigMaxPositionEmbeddings > 0 {
+ kv["mistral3.rope.scaling.original_context_length"] = p.TextModel.RopeParameters.OrigMaxPositionEmbeddings
+ }
+ if p.TextModel.RopeParameters.Llama4ScalingBeta != nil {
+ kv["mistral3.rope.scaling_beta"] = *p.TextModel.RopeParameters.Llama4ScalingBeta
+ }
// Vision configuration
kv["mistral3.vision.block_count"] = p.VisionModel.NumHiddenLayers
@@ -74,7 +105,7 @@ func (p *mistral3Model) KV(t *Tokenizer) ggml.KV {
kv["mistral3.vision.patch_size"] = p.VisionModel.PatchSize
kv["mistral3.vision.num_channels"] = p.VisionModel.NumChannels
// kv["mistral3.vision.attention.layer_norm_epsilon"] = 1e-05 // Default value
- kv["mistral3.vision.rope.freq_base"] = p.VisionModel.RopeTheta
+ kv["mistral3.vision.rope.freq_base"] = cmp.Or(p.VisionModel.RopeTheta, p.VisionModel.RopeParameters.RopeTheta)
// Multimodal configuration
kv["mistral3.image_token_index"] = p.ImageTokenIndex
diff --git a/convert/convert_mistral_causal.go b/convert/convert_mistral_causal.go
new file mode 100644
index 00000000000..99a483736d8
--- /dev/null
+++ b/convert/convert_mistral_causal.go
@@ -0,0 +1,181 @@
+package convert
+
+import (
+ "cmp"
+ "fmt"
+ "strings"
+
+ "github.com/pdevine/tensor"
+ "github.com/pdevine/tensor/native"
+
+ "github.com/ollama/ollama/fs/ggml"
+)
+
+type mistral3CausalModel struct {
+ ModelParameters
+
+ NumHiddenLayers uint32 `json:"num_hidden_layers"`
+ MaxPositionEmbeddings uint32 `json:"max_position_embeddings"`
+ HiddenSize uint32 `json:"hidden_size"`
+ IntermediateSize uint32 `json:"intermediate_size"`
+ NumAttentionHeads uint32 `json:"num_attention_heads"`
+ NumKeyValueHeads uint32 `json:"num_key_value_heads"`
+ RopeTheta float32 `json:"rope_theta"`
+ RMSNormEPS float32 `json:"rms_norm_eps"`
+ HeadDim uint32 `json:"head_dim"`
+ SlidingWindow *uint32 `json:"sliding_window"`
+ HiddenAct string `json:"hidden_act"`
+ VocabSize uint32 `json:"vocab_size"`
+ RopeParameters struct {
+ BetaFast float32 `json:"beta_fast"`
+ BetaSlow float32 `json:"beta_slow"`
+ Factor float32 `json:"factor"`
+ Llama4ScalingBeta *float32 `json:"llama_4_scaling_beta"`
+ OrigMaxPositionEmbeddings uint32 `json:"original_max_position_embeddings"`
+ RopeType string `json:"rope_type"`
+ RopeTheta float32 `json:"rope_theta"`
+ Mscale *float32 `json:"mscale"`
+ MscaleAllDim *float32 `json:"mscale_all_dim"`
+ } `json:"rope_parameters"`
+}
+
+func (p *mistral3CausalModel) KV(t *Tokenizer) ggml.KV {
+ kv := p.ModelParameters.KV(t)
+ kv["general.architecture"] = "mistral3"
+ kv["mistral3.vocab_size"] = p.VocabSize
+
+ // Text configuration
+ kv["mistral3.block_count"] = p.NumHiddenLayers
+ kv["mistral3.context_length"] = p.MaxPositionEmbeddings
+ kv["mistral3.embedding_length"] = p.HiddenSize
+ kv["mistral3.feed_forward_length"] = p.IntermediateSize
+ kv["mistral3.attention.head_count"] = p.NumAttentionHeads
+ kv["mistral3.attention.head_count_kv"] = p.NumKeyValueHeads
+ kv["mistral3.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
+ kv["mistral3.attention.key_length"] = p.HeadDim
+ kv["mistral3.attention.value_length"] = p.HeadDim
+ kv["mistral3.rope.dimension_count"] = cmp.Or(p.HeadDim, p.HiddenSize/p.NumAttentionHeads)
+ kv["mistral3.rope.freq_base"] = cmp.Or(p.RopeTheta, p.RopeParameters.RopeTheta)
+ kv["mistral3.rope.scaling.factor"] = p.RopeParameters.Factor
+ kv["mistral3.rope.scaling.type"] = p.RopeParameters.RopeType
+ kv["mistral3.rope.scaling.beta_fast"] = p.RopeParameters.BetaFast
+ kv["mistral3.rope.scaling.beta_slow"] = p.RopeParameters.BetaSlow
+
+ if p.RopeParameters.Mscale != nil {
+ kv["mistral3.rope.scaling.mscale"] = *p.RopeParameters.Mscale
+ }
+
+ if p.RopeParameters.MscaleAllDim != nil {
+ kv["mistral3.rope.scaling.mscale_all_dim"] = *p.RopeParameters.MscaleAllDim
+ }
+
+ if p.RopeParameters.OrigMaxPositionEmbeddings > 0 {
+ kv["mistral3.rope.scaling.original_context_length"] = p.RopeParameters.OrigMaxPositionEmbeddings
+ kv["mistral3.rope.scaling_beta"] = *p.RopeParameters.Llama4ScalingBeta
+ }
+
+ if p.RopeParameters.Llama4ScalingBeta != nil {
+ kv["mistral3.rope.scaling_beta"] = *p.RopeParameters.Llama4ScalingBeta
+ }
+
+ return kv
+}
+
+func (p *mistral3CausalModel) Tensors(ts []Tensor) []*ggml.Tensor {
+ var out []*ggml.Tensor
+
+ for _, t := range ts {
+ if !strings.HasPrefix(t.Name(), "v.") {
+ if strings.HasSuffix(t.Name(), ".attn_q.weight") ||
+ strings.HasSuffix(t.Name(), ".attn_k.weight") {
+ t.SetRepacker(p.repack)
+ }
+ }
+
+ out = append(out, &ggml.Tensor{
+ Name: t.Name(),
+ Kind: t.Kind(),
+ Shape: t.Shape(),
+ WriterTo: t,
+ })
+ }
+
+ return out
+}
+
+func (p *mistral3CausalModel) Replacements() []string {
+ return []string{
+ "model.norm", "output_norm",
+ "model.", "",
+ "layers", "blk",
+ "transformer.layers", "blk",
+ "vision_tower", "v",
+ "ln_pre", "encoder_norm",
+ "input_layernorm", "attn_norm",
+ "post_attention_layernorm", "ffn_norm",
+ "embed_tokens", "token_embd",
+ "self_attn.q_proj", "attn_q",
+ "self_attn.k_proj", "attn_k",
+ "self_attn.v_proj", "attn_v",
+ "self_attn.o_proj", "attn_output",
+ "mlp.down_proj", "ffn_down",
+ "mlp.gate_proj", "ffn_gate",
+ "mlp.up_proj", "ffn_up",
+ "attention.q_proj", "attn_q",
+ "attention.k_proj", "attn_k",
+ "attention.v_proj", "attn_v",
+ "attention.o_proj", "attn_output",
+ "attention_norm", "attn_norm",
+ "feed_forward.gate_proj", "ffn_gate",
+ "feed_forward.down_proj", "ffn_down",
+ "feed_forward.up_proj", "ffn_up",
+ "multi_modal_projector", "mm",
+ "ffn_norm", "ffn_norm",
+ "lm_head", "output",
+ }
+}
+
+func (p *mistral3CausalModel) repack(name string, data []float32, shape []uint64) ([]float32, error) {
+ var dims []int
+ for _, dim := range shape {
+ dims = append(dims, int(dim))
+ }
+
+ var heads uint32
+ if strings.HasSuffix(name, ".attn_q.weight") {
+ heads = p.NumAttentionHeads
+ } else if strings.HasSuffix(name, ".attn_k.weight") {
+ heads = cmp.Or(p.NumKeyValueHeads, p.NumAttentionHeads)
+ } else {
+ return nil, fmt.Errorf("unknown tensor for repack: %s", name)
+ }
+
+ n := tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
+ if err := n.Reshape(append([]int{int(heads), 2, dims[0] / int(heads) / 2}, dims[1:]...)...); err != nil {
+ return nil, err
+ }
+
+ if err := n.T(0, 2, 1, 3); err != nil {
+ return nil, err
+ }
+
+ if err := n.Reshape(dims...); err != nil {
+ return nil, err
+ }
+
+ if err := n.Transpose(); err != nil {
+ return nil, err
+ }
+
+ ts, err := native.SelectF32(n, 1)
+ if err != nil {
+ return nil, err
+ }
+
+ var f32s []float32
+ for _, t := range ts {
+ f32s = append(f32s, t...)
+ }
+
+ return f32s, nil
+}
diff --git a/convert/convert_nomicbert.go b/convert/convert_nomicbert.go
new file mode 100644
index 00000000000..6aed5ee75ec
--- /dev/null
+++ b/convert/convert_nomicbert.go
@@ -0,0 +1,213 @@
+package convert
+
+import (
+ "cmp"
+ "encoding/json"
+ "io/fs"
+ "path/filepath"
+ "slices"
+ "strings"
+
+ "github.com/ollama/ollama/fs/ggml"
+)
+
+type nomicbertModel struct {
+ ModelParameters
+ NLayers uint32 `json:"n_layers"`
+ NumHiddenLayers uint32 `json:"num_hidden_layers"`
+ MaxPositionEmbeddings uint32 `json:"max_position_embeddings"`
+ HiddenSize uint32 `json:"hidden_size"`
+ IntermediateSize uint32 `json:"intermediate_size"`
+ NumAttentionHeads uint32 `json:"num_attention_heads"`
+ NumKeyValueHeads uint32 `json:"num_key_value_heads"`
+ LayerNormEPS float32 `json:"layer_norm_eps"`
+ LayerNormEpsilon float32 `json:"layer_norm_epsilon"`
+ RopeFreqBase float32 `json:"rope_theta"`
+ normalizeEmbeddings bool
+ PoolingType uint32
+
+ // MoE parameters (only present in v2 models)
+ NumExperts uint32 `json:"num_local_experts"`
+ NumExpertsUsed uint32 `json:"num_experts_per_tok"`
+ MoEEveryNLayers uint32 `json:"moe_every_n_layers"`
+}
+
+var (
+ _ ModelConverter = (*nomicbertModel)(nil)
+ _ moreParser = (*nomicbertModel)(nil)
+)
+
+func (p *nomicbertModel) parseMore(fsys fs.FS) error {
+ bts, err := fs.ReadFile(fsys, "modules.json")
+ if err != nil {
+ return err
+ }
+
+ var modules []struct {
+ Type string `json:"type"`
+ Path string `json:"path"`
+ }
+
+ if err := json.Unmarshal(bts, &modules); err != nil {
+ return err
+ }
+
+ var pooling string
+ for _, m := range modules {
+ switch m.Type {
+ case "sentence_transformers.models.Pooling":
+ pooling = m.Path
+ case "sentence_transformers.models.Normalize":
+ p.normalizeEmbeddings = true
+ }
+ }
+
+ if pooling != "" {
+ bts, err := fs.ReadFile(fsys, filepath.Join(pooling, "config.json"))
+ if err != nil {
+ return err
+ }
+
+ var pc struct {
+ PoolingModeCLSToken bool `json:"pooling_mode_cls_token"`
+ PoolingModeMeanTokens bool `json:"pooling_mode_mean_tokens"`
+ }
+
+ if err := json.Unmarshal(bts, &pc); err != nil {
+ return err
+ }
+
+ if pc.PoolingModeMeanTokens {
+ p.PoolingType = 1
+ } else if pc.PoolingModeCLSToken {
+ p.PoolingType = 2
+ }
+ }
+
+ return nil
+}
+
+func (p *nomicbertModel) KV(t *Tokenizer) ggml.KV {
+ kv := p.ModelParameters.KV(t)
+
+ // Determine architecture based on MoE parameters (following qwen3 pattern)
+ arch := "nomic-bert"
+ if p.MoEEveryNLayers > 0 {
+ arch += "-moe"
+ }
+
+ kv["general.architecture"] = arch
+ kv["attention.causal"] = false
+ kv["pooling_type"] = p.PoolingType
+ kv["normalize_embeddings"] = p.normalizeEmbeddings
+
+ kv["block_count"] = cmp.Or(p.NLayers, p.NumHiddenLayers)
+
+ if contextLength := p.MaxPositionEmbeddings; contextLength > 0 {
+ kv["context_length"] = contextLength
+ }
+
+ if embeddingLength := p.HiddenSize; embeddingLength > 0 {
+ kv["embedding_length"] = p.HiddenSize
+ }
+
+ if feedForwardLength := p.IntermediateSize; feedForwardLength > 0 {
+ kv["feed_forward_length"] = p.IntermediateSize
+ }
+
+ if headCount := p.NumAttentionHeads; headCount > 0 {
+ kv["attention.head_count"] = p.NumAttentionHeads
+ }
+
+ if kvHeadCount := p.NumKeyValueHeads; kvHeadCount > 0 {
+ kv["attention.head_count_kv"] = p.NumKeyValueHeads
+ }
+
+ if layerNormEpsilon := cmp.Or(p.LayerNormEPS, p.LayerNormEpsilon); layerNormEpsilon > 0 {
+ kv["attention.layer_norm_epsilon"] = layerNormEpsilon
+ }
+
+ if p.RopeFreqBase > 0 {
+ kv["rope.freq_base"] = p.RopeFreqBase
+ }
+
+ // MoE specific parameters (only if MoE is enabled)
+ if p.NumExperts > 0 {
+ kv["expert_count"] = p.NumExperts
+ }
+
+ if p.NumExpertsUsed > 0 {
+ kv["expert_used_count"] = p.NumExpertsUsed
+ }
+
+ if p.MoEEveryNLayers > 0 {
+ kv["moe_every_n_layers"] = p.MoEEveryNLayers
+ }
+
+ kv["tokenizer.ggml.model"] = "bert"
+ kv["tokenizer.ggml.token_type_count"] = uint32(2)
+
+ // convert to phantom space tokens
+ for i, e := range t.Tokens {
+ switch {
+ case strings.HasPrefix(e, "[") && strings.HasSuffix(e, "]"):
+ // noop - keep special tokens as-is
+ case strings.HasPrefix(e, "##"):
+ t.Tokens[i] = e[2:]
+ default:
+ t.Tokens[i] = "\u2581" + e
+ }
+ }
+
+ kv["tokenizer.ggml.tokens"] = t.Tokens
+
+ return kv
+}
+
+func (p *nomicbertModel) Tensors(ts []Tensor) []*ggml.Tensor {
+ out := make([]*ggml.Tensor, 0, len(ts))
+ for _, t := range ts {
+ if slices.Contains([]string{
+ "embeddings.position_ids",
+ "pooler.dense.weight",
+ "pooler.dense.bias",
+ }, t.Name()) {
+ continue
+ }
+
+ out = append(out, &ggml.Tensor{
+ Name: t.Name(),
+ Kind: t.Kind(),
+ Shape: t.Shape(),
+ WriterTo: t,
+ })
+ }
+
+ return out
+}
+
+func (nomicbertModel) Replacements() []string {
+ return []string{
+ "encoder.layer", "blk",
+ "encoder.layers", "blk",
+ "embeddings.word_embeddings", "token_embd",
+ "embeddings.token_type_embeddings", "token_types",
+ "embeddings.LayerNorm", "token_embd_norm",
+
+ "attention.self.qkv", "attn_qkv",
+
+ "attention.output.dense", "attn_output",
+ "attention.output.LayerNorm", "attn_output_norm",
+
+ "mlp.up", "ffn_up",
+ "mlp.down", "ffn_down",
+
+ "mlp.router", "ffn_gate_inp",
+ "mlp.experts.up", "ffn_up_exps",
+ "mlp.experts.down", "ffn_down_exps",
+
+ "intermediate.dense", "ffn_up",
+ "output.dense", "ffn_down",
+ "output.LayerNorm", "layer_output_norm",
+ }
+}
diff --git a/convert/convert_olmo.go b/convert/convert_olmo.go
new file mode 100644
index 00000000000..f75c6847730
--- /dev/null
+++ b/convert/convert_olmo.go
@@ -0,0 +1,117 @@
+package convert
+
+import (
+ "cmp"
+
+ "github.com/ollama/ollama/fs/ggml"
+)
+
+type ropeScaling struct {
+ Factor float32 `json:"factor"`
+ OriginalMaxPositionEmbeds uint32 `json:"original_max_position_embeddings"`
+ AttentionFactor float32 `json:"attention_factor"`
+ BetaFast float32 `json:"beta_fast"`
+ BetaSlow float32 `json:"beta_slow"`
+ RopeType string `json:"rope_type"`
+ ExtrapolationFactor float32 `json:"extrapolation_factor"`
+}
+
+type olmoModel struct {
+ ModelParameters
+
+ HiddenSize uint32 `json:"hidden_size"`
+ NumHiddenLayers uint32 `json:"num_hidden_layers"`
+ IntermediateSize uint32 `json:"intermediate_size"`
+ NumAttentionHeads uint32 `json:"num_attention_heads"`
+ NumKeyValueHeads uint32 `json:"num_key_value_heads"`
+ MaxPositionEmbeddings uint32 `json:"max_position_embeddings"`
+ RMSNormEPS float32 `json:"rms_norm_eps"`
+ RopeTheta float32 `json:"rope_theta"`
+ RopeScaling *ropeScaling `json:"rope_scaling"`
+ SlidingWindow uint32 `json:"sliding_window"`
+ LayerTypes []string `json:"layer_types"`
+}
+
+var _ ModelConverter = (*olmoModel)(nil)
+
+func (p *olmoModel) KV(t *Tokenizer) ggml.KV {
+ kv := p.ModelParameters.KV(t)
+ kv["general.architecture"] = "olmo3"
+ kv["olmo3.block_count"] = p.NumHiddenLayers
+ kv["olmo3.context_length"] = p.MaxPositionEmbeddings
+ kv["olmo3.embedding_length"] = p.HiddenSize
+ kv["olmo3.feed_forward_length"] = p.IntermediateSize
+ kv["olmo3.attention.head_count"] = p.NumAttentionHeads
+ kv["olmo3.attention.head_count_kv"] = cmp.Or(p.NumKeyValueHeads, p.NumAttentionHeads)
+
+ if p.RopeTheta > 0 {
+ kv["olmo3.rope.freq_base"] = p.RopeTheta
+ }
+
+ if p.RopeScaling != nil {
+ if p.RopeScaling.Factor > 0 {
+ kv["olmo3.rope.scaling.factor"] = p.RopeScaling.Factor
+ }
+ if p.RopeScaling.OriginalMaxPositionEmbeds > 0 {
+ kv["olmo3.rope.scaling.original_context_length"] = p.RopeScaling.OriginalMaxPositionEmbeds
+ }
+ if p.RopeScaling.AttentionFactor > 0 {
+ kv["olmo3.rope.scaling.attn_factor"] = p.RopeScaling.AttentionFactor
+ }
+ if p.RopeScaling.RopeType != "" {
+ kv["olmo3.rope.scaling.type"] = p.RopeScaling.RopeType
+ }
+ }
+
+ if p.RMSNormEPS > 0 {
+ kv["olmo3.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
+ }
+
+ if p.SlidingWindow > 0 {
+ kv["olmo3.attention.sliding_window"] = p.SlidingWindow
+ }
+
+ if len(p.LayerTypes) > 0 {
+ slidingPattern := make([]bool, len(p.LayerTypes))
+ for i, layerType := range p.LayerTypes {
+ slidingPattern[i] = (layerType == "sliding_attention")
+ }
+ kv["olmo3.attention.sliding_window_pattern"] = slidingPattern
+ }
+
+ return kv
+}
+
+func (p *olmoModel) Tensors(ts []Tensor) []*ggml.Tensor {
+ out := make([]*ggml.Tensor, 0, len(ts))
+ for _, t := range ts {
+ out = append(out, &ggml.Tensor{
+ Name: t.Name(),
+ Kind: t.Kind(),
+ Shape: t.Shape(),
+ WriterTo: t,
+ })
+ }
+
+ return out
+}
+
+func (p *olmoModel) Replacements() []string {
+ return []string{
+ "lm_head", "output",
+ "model.embed_tokens", "token_embd",
+ "model.layers", "blk",
+ "model.norm", "output_norm",
+ "self_attn.q_proj", "attn_q",
+ "self_attn.k_proj", "attn_k",
+ "self_attn.v_proj", "attn_v",
+ "self_attn.o_proj", "attn_output",
+ "self_attn.q_norm", "attn_q_norm",
+ "self_attn.k_norm", "attn_k_norm",
+ "post_attention_layernorm", "post_attention_norm",
+ "post_feedforward_layernorm", "post_ffw_norm",
+ "mlp.gate_proj", "ffn_gate",
+ "mlp.down_proj", "ffn_down",
+ "mlp.up_proj", "ffn_up",
+ }
+}
diff --git a/convert/reader.go b/convert/reader.go
index b3f7a866039..75764f018b9 100644
--- a/convert/reader.go
+++ b/convert/reader.go
@@ -44,7 +44,10 @@ func (t tensorBase) Kind() uint32 {
t.name == "v.positional_embedding_vlm" ||
t.name == "v.tile_position_embd.weight" ||
t.name == "v.pre_tile_position_embd.weight" ||
- t.name == "v.post_tile_position_embd.weight" {
+ t.name == "v.post_tile_position_embd.weight" ||
+ t.name == "s.position_embd" ||
+ strings.HasSuffix(t.name, "rel_pos_h") ||
+ strings.HasSuffix(t.name, "rel_pos_w") {
// these tensors are always F32
return tensorKindFP32
}
diff --git a/convert/reader_safetensors.go b/convert/reader_safetensors.go
index eea0de2f506..f7d9754f03c 100644
--- a/convert/reader_safetensors.go
+++ b/convert/reader_safetensors.go
@@ -96,7 +96,10 @@ type safetensor struct {
func (st safetensor) Kind() uint32 {
kind := st.tensorBase.Kind()
- if !strings.HasPrefix(st.name, "v.") && st.dtype == "BF16" && kind != tensorKindFP32 {
+ if st.dtype == "BF16" &&
+ !strings.HasPrefix(st.name, "v.") &&
+ !strings.HasPrefix(st.name, "s.") &&
+ kind != tensorKindFP32 {
kind = tensorKindBF16
}
diff --git a/convert/tokenizer_spm.go b/convert/tokenizer_spm.go
index 340c3d581fb..51efb35e4be 100644
--- a/convert/tokenizer_spm.go
+++ b/convert/tokenizer_spm.go
@@ -49,7 +49,8 @@ func parseSentencePiece(fsys fs.FS) (*Vocabulary, error) {
tt := int32(sentencepiece.ModelProto_SentencePiece_NORMAL)
// temporary fix to handle gemma3 broken configs
- if slices.Contains([]string{"", ""}, piece.GetPiece()) {
+ // TODO(parthsareen): allow reading of tokenizer.json to allow managing special tokens when using spm
+ if slices.Contains([]string{"", "", "", "", "", "", "", "", ""}, piece.GetPiece()) {
tt = int32(sentencepiece.ModelProto_SentencePiece_CONTROL)
}
diff --git a/discover/cpu_linux.go b/discover/cpu_linux.go
index c3a0ef7fa3d..e4ea94f74cc 100644
--- a/discover/cpu_linux.go
+++ b/discover/cpu_linux.go
@@ -2,6 +2,7 @@ package discover
import (
"bufio"
+ "errors"
"fmt"
"io"
"log/slog"
@@ -10,12 +11,21 @@ import (
"reflect"
"regexp"
"sort"
+ "strconv"
"strings"
"github.com/ollama/ollama/format"
)
func GetCPUMem() (memInfo, error) {
+ mem, err := getCPUMem()
+ if err != nil {
+ return memInfo{}, err
+ }
+ return getCPUMemByCgroups(mem), nil
+}
+
+func getCPUMem() (memInfo, error) {
var mem memInfo
var total, available, free, buffers, cached, freeSwap uint64
f, err := os.Open("/proc/meminfo")
@@ -56,6 +66,32 @@ func GetCPUMem() (memInfo, error) {
return mem, nil
}
+func getCPUMemByCgroups(mem memInfo) memInfo {
+ total, err := getUint64ValueFromFile("/sys/fs/cgroup/memory.max")
+ if err == nil {
+ mem.TotalMemory = total
+ }
+ used, err := getUint64ValueFromFile("/sys/fs/cgroup/memory.current")
+ if err == nil {
+ mem.FreeMemory = mem.TotalMemory - used
+ }
+ return mem
+}
+
+func getUint64ValueFromFile(path string) (uint64, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return 0, err
+ }
+ defer f.Close()
+ s := bufio.NewScanner(f)
+ for s.Scan() {
+ line := s.Text()
+ return strconv.ParseUint(line, 10, 64)
+ }
+ return 0, errors.New("empty file content")
+}
+
const CpuInfoFilename = "/proc/cpuinfo"
type linuxCpuInfo struct {
@@ -74,7 +110,41 @@ func GetCPUDetails() []CPU {
return nil
}
defer file.Close()
- return linuxCPUDetails(file)
+ cpus := linuxCPUDetails(file)
+ return overwriteThreadCountByLinuxCgroups(cpus)
+}
+
+func overwriteThreadCountByLinuxCgroups(cpus []CPU) []CPU {
+ file, err := os.Open("/sys/fs/cgroup/cpu.max")
+ if err != nil {
+ return cpus
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if sl := strings.Split(line, " "); len(sl) == 2 {
+ allowdUs, err := strconv.ParseInt(sl[0], 10, 64)
+ if err != nil {
+ slog.Warn("failed to parse CPU allowed micro secs", "error", err)
+ return cpus
+ }
+ unitUs, err := strconv.ParseInt(sl[1], 10, 64)
+ if err != nil {
+ slog.Warn("failed to parse CPU unit micro secs", "error", err)
+ return cpus
+ }
+
+ threads := int(max(allowdUs/unitUs, 1))
+
+ cpu := cpus[0]
+ cpu.CoreCount = threads
+ cpu.ThreadCount = threads
+ return []CPU{cpu}
+ }
+ }
+ return cpus
}
func linuxCPUDetails(file io.Reader) []CPU {
diff --git a/discover/runner.go b/discover/runner.go
index bf2110bc772..e0b6582370c 100644
--- a/discover/runner.go
+++ b/discover/runner.go
@@ -65,6 +65,11 @@ func GPUDevices(ctx context.Context, runners []ml.FilteredRunnerDiscovery) []ml.
}
slog.Info("discovering available GPUs...")
+ detectIncompatibleLibraries()
+
+ // Warn if any user-overrides are set which could lead to incorrect GPU discovery
+ overrideWarnings()
+
requested := envconfig.LLMLibrary()
jetpack := cudaJetpack()
@@ -90,10 +95,13 @@ func GPUDevices(ctx context.Context, runners []ml.FilteredRunnerDiscovery) []ml.
var dirs []string
if dir != "" {
if requested != "" && filepath.Base(dir) != requested {
- slog.Debug("skipping available library at users request", "requested", requested, "libDir", dir)
+ slog.Debug("skipping available library at user's request", "requested", requested, "libDir", dir)
continue
} else if jetpack != "" && filepath.Base(dir) != "cuda_"+jetpack {
continue
+ } else if jetpack == "" && strings.Contains(filepath.Base(dir), "cuda_jetpack") {
+ slog.Debug("jetpack not detected (set JETSON_JETPACK or OLLAMA_LLM_LIBRARY to override), skipping", "libDir", dir)
+ continue
} else if !envconfig.EnableVulkan() && strings.Contains(filepath.Base(dir), "vulkan") {
slog.Info("experimental Vulkan support disabled. To enable, set OLLAMA_VULKAN=1")
continue
@@ -113,7 +121,7 @@ func GPUDevices(ctx context.Context, runners []ml.FilteredRunnerDiscovery) []ml.
// In the second pass, we more deeply initialize the GPUs to weed out devices that
// aren't supported by a given library. We run this phase in parallel to speed up discovery.
// Only devices that need verification are included in this pass
- slog.Debug("evluating which if any devices to filter out", "initial_count", len(devices))
+ slog.Debug("evaluating which, if any, devices to filter out", "initial_count", len(devices))
ctx2ndPass, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
var wg sync.WaitGroup
@@ -121,15 +129,25 @@ func GPUDevices(ctx context.Context, runners []ml.FilteredRunnerDiscovery) []ml.
supportedMu := sync.Mutex{}
supported := make(map[string]map[string]map[string]int) // [Library][libDir][ID] = pre-deletion devices index
for i := range devices {
+ libDir := devices[i].LibraryPath[len(devices[i].LibraryPath)-1]
if !devices[i].NeedsInitValidation() {
+ // No need to validate, add to the supported map
+ supportedMu.Lock()
+ if _, ok := supported[devices[i].Library]; !ok {
+ supported[devices[i].Library] = make(map[string]map[string]int)
+ }
+ if _, ok := supported[devices[i].Library][libDir]; !ok {
+ supported[devices[i].Library][libDir] = make(map[string]int)
+ }
+ supported[devices[i].Library][libDir][devices[i].ID] = i
+ supportedMu.Unlock()
continue
}
- libDir := devices[i].LibraryPath[len(devices[i].LibraryPath)-1]
- slog.Debug("verifying device is supported", "library", libDir, "description", devices[i].Description, "compute", devices[i].Compute(), "id", devices[i].ID, "pci_id", devices[i].PCIID)
+ slog.Debug("verifying if device is supported", "library", libDir, "description", devices[i].Description, "compute", devices[i].Compute(), "id", devices[i].ID, "pci_id", devices[i].PCIID)
wg.Add(1)
go func(i int) {
defer wg.Done()
- extraEnvs := ml.GetVisibleDevicesEnv(devices[i : i+1])
+ extraEnvs := ml.GetVisibleDevicesEnv(devices[i:i+1], true)
devices[i].AddInitValidation(extraEnvs)
if len(bootstrapDevices(ctx2ndPass, devices[i].LibraryPath, extraEnvs)) == 0 {
slog.Debug("filtering device which didn't fully initialize",
@@ -315,7 +333,8 @@ func GPUDevices(ctx context.Context, runners []ml.FilteredRunnerDiscovery) []ml.
defer cancel()
// Apply any dev filters to avoid re-discovering unsupported devices, and get IDs correct
- devFilter := ml.GetVisibleDevicesEnv(devices)
+ // We avoid CUDA filters here to keep ROCm from failing to discover GPUs in a mixed environment
+ devFilter := ml.GetVisibleDevicesEnv(devices, false)
for dir := range libDirs {
updatedDevices := bootstrapDevices(ctx, []string{ml.LibOllamaPath, dir}, devFilter)
@@ -422,6 +441,7 @@ func bootstrapDevices(ctx context.Context, ollamaLibDirs []string, extraEnvs map
cmd, port, err := llm.StartRunner(
true, // ollama engine
"", // no model
+ make([]string, 0),
ollamaLibDirs,
out,
extraEnvs,
@@ -449,3 +469,37 @@ func bootstrapDevices(ctx context.Context, ollamaLibDirs []string, extraEnvs map
return devices
}
+
+func overrideWarnings() {
+ anyFound := false
+ m := envconfig.AsMap()
+ for _, k := range []string{
+ "CUDA_VISIBLE_DEVICES",
+ "HIP_VISIBLE_DEVICES",
+ "ROCR_VISIBLE_DEVICES",
+ "GGML_VK_VISIBLE_DEVICES",
+ "GPU_DEVICE_ORDINAL",
+ "HSA_OVERRIDE_GFX_VERSION",
+ } {
+ if e, found := m[k]; found && e.Value != "" {
+ anyFound = true
+ slog.Warn("user overrode visible devices", k, e.Value)
+ }
+ }
+ if anyFound {
+ slog.Warn("if GPUs are not correctly discovered, unset and try again")
+ }
+}
+
+func detectIncompatibleLibraries() {
+ if runtime.GOOS != "windows" {
+ return
+ }
+ basePath, err := exec.LookPath("ggml-base.dll")
+ if err != nil || basePath == "" {
+ return
+ }
+ if !strings.HasPrefix(basePath, ml.LibOllamaPath) {
+ slog.Warn("potentially incompatible library detected in PATH", "location", basePath)
+ }
+}
diff --git a/docs/api.md b/docs/api.md
index 3b106a66cd0..8d587924691 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -50,7 +50,7 @@ Generate a response for a given prompt with a provided model. This is a streamin
Advanced parameters (optional):
- `format`: the format to return a response in. Format can be `json` or a JSON schema
-- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.md#valid-parameters-and-values) such as `temperature`
+- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.mdx#valid-parameters-and-values) such as `temperature`
- `system`: system message to (overrides what is defined in the `Modelfile`)
- `template`: the prompt template to use (overrides what is defined in the `Modelfile`)
- `stream`: if `false` the response will be returned as a single response object, rather than a stream of objects
@@ -508,7 +508,7 @@ The `message` object has the following fields:
Advanced parameters (optional):
- `format`: the format to return a response in. Format can be `json` or a JSON schema.
-- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.md#valid-parameters-and-values) such as `temperature`
+- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.mdx#valid-parameters-and-values) such as `temperature`
- `stream`: if `false` the response will be returned as a single response object, rather than a stream of objects
- `keep_alive`: controls how long the model will stay loaded into memory following the request (default: `5m`)
@@ -896,11 +896,11 @@ curl http://localhost:11434/api/chat -d '{
"tool_calls": [
{
"function": {
- "name": "get_temperature",
+ "name": "get_weather",
"arguments": {
"city": "Toronto"
}
- },
+ }
}
]
},
@@ -908,7 +908,7 @@ curl http://localhost:11434/api/chat -d '{
{
"role": "tool",
"content": "11 degrees celsius",
- "tool_name": "get_temperature",
+ "tool_name": "get_weather"
}
],
"stream": false,
@@ -1177,7 +1177,7 @@ Create a model from:
- another model;
- a safetensors directory; or
-- a GGUF file.
+- a GGUF file or directory.
If you are creating a model from a safetensors directory or from a GGUF file, you must [create a blob](#create-a-blob) for each of the files and then use the file name and SHA256 digest associated with each blob in the `files` field.
@@ -1190,7 +1190,7 @@ If you are creating a model from a safetensors directory or from a GGUF file, yo
- `template`: (optional) the prompt template for the model
- `license`: (optional) a string or list of strings containing the license or licenses for the model
- `system`: (optional) a string containing the system prompt for the model
-- `parameters`: (optional) a dictionary of parameters for the model (see [Modelfile](./modelfile.md#valid-parameters-and-values) for a list of parameters)
+- `parameters`: (optional) a dictionary of parameters for the model (see [Modelfile](./modelfile.mdx#valid-parameters-and-values) for a list of parameters)
- `messages`: (optional) a list of message objects used to create a conversation
- `stream`: (optional) if `false` the response will be returned as a single response object, rather than a stream of objects
- `quantize` (optional): quantize a non-quantized (e.g. float16) model
@@ -1271,6 +1271,7 @@ A stream of JSON objects is returned:
#### Create a model from GGUF
Create a model from a GGUF file. The `files` parameter should be filled out with the file name and SHA256 digest of the GGUF file you wish to use. Use [/api/blobs/:digest](#push-a-blob) to push the GGUF file to the server before calling this API.
+For a model stored in multiple split GGUF files, includes all split GGUF files in the `files` parameter with the file names and SHA256 digests. It is recommended to provide files in split number order even though Ollama itself will sort them in order.
##### Request
@@ -1699,7 +1700,7 @@ Generate embeddings from a model
Advanced parameters:
- `truncate`: truncates the end of each input to fit within context length. Returns error if `false` and context length is exceeded. Defaults to `true`
-- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.md#valid-parameters-and-values) such as `temperature`
+- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.mdx#valid-parameters-and-values) such as `temperature`
- `keep_alive`: controls how long the model will stay loaded into memory following the request (default: `5m`)
- `dimensions`: number of dimensions for the embedding
@@ -1818,7 +1819,7 @@ Generate embeddings from a model
Advanced parameters:
-- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.md#valid-parameters-and-values) such as `temperature`
+- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.mdx#valid-parameters-and-values) such as `temperature`
- `keep_alive`: controls how long the model will stay loaded into memory following the request (default: `5m`)
### Examples
diff --git a/docs/api/openai-compatibility.mdx b/docs/api/openai-compatibility.mdx
index 8329934af1b..a0882053e8e 100644
--- a/docs/api/openai-compatibility.mdx
+++ b/docs/api/openai-compatibility.mdx
@@ -6,16 +6,16 @@ Ollama provides compatibility with parts of the [OpenAI API](https://platform.op
## Usage
-### OpenAI Python library
+### Simple `v1/chat/completions` example
-```python
+
+
+```python basic.py
from openai import OpenAI
client = OpenAI(
base_url='http://localhost:11434/v1/',
-
- # required but ignored
- api_key='ollama',
+ api_key='ollama', # required but ignored
)
chat_completion = client.chat.completions.create(
@@ -25,96 +25,125 @@ chat_completion = client.chat.completions.create(
'content': 'Say this is a test',
}
],
- model='llama3.2',
+ model='gpt-oss:20b',
)
+print(chat_completion.choices[0].message.content)
+```
-response = client.chat.completions.create(
- model="llava",
- messages=[
- {
- "role": "user",
- "content": [
- {"type": "text", "text": "What's in this image?"},
- {
- "type": "image_url",
- "image_url": "data:image/png;base64,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",
- },
- ],
- }
- ],
- max_tokens=300,
-)
+```javascript basic.js
+import OpenAI from "openai";
-completion = client.completions.create(
- model="llama3.2",
- prompt="Say this is a test",
-)
+const openai = new OpenAI({
+ baseURL: "http://localhost:11434/v1/",
+ apiKey: "ollama", // required but ignored
+});
-list_completion = client.models.list()
+const chatCompletion = await openai.chat.completions.create({
+ messages: [{ role: "user", content: "Say this is a test" }],
+ model: "gpt-oss:20b",
+});
-model = client.models.retrieve("llama3.2")
+console.log(chatCompletion.choices[0].message.content);
+```
-embeddings = client.embeddings.create(
- model="all-minilm",
- input=["why is the sky blue?", "why is the grass green?"],
-)
+```shell basic.sh
+curl -X POST http://localhost:11434/v1/chat/completions \
+-H "Content-Type: application/json" \
+-d '{
+ "model": "gpt-oss:20b",
+ "messages": [{ "role": "user", "content": "Say this is a test" }]
+}'
```
-#### Structured outputs
+
+
+### Simple `v1/responses` example
-```python
-from pydantic import BaseModel
+
+
+```python responses.py
from openai import OpenAI
-client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
-
-# Define the schema for the response
-class FriendInfo(BaseModel):
- name: str
- age: int
- is_available: bool
-
-class FriendList(BaseModel):
- friends: list[FriendInfo]
-
-try:
- completion = client.beta.chat.completions.parse(
- temperature=0,
- model="llama3.1:8b",
- messages=[
- {"role": "user", "content": "I have two friends. The first is Ollama 22 years old busy saving the world, and the second is Alonso 23 years old and wants to hang out. Return a list of friends in JSON format"}
- ],
- response_format=FriendList,
- )
-
- friends_response = completion.choices[0].message
- if friends_response.parsed:
- print(friends_response.parsed)
- elif friends_response.refusal:
- print(friends_response.refusal)
-except Exception as e:
- print(f"Error: {e}")
-```
+client = OpenAI(
+ base_url='http://localhost:11434/v1/',
+ api_key='ollama', # required but ignored
+)
-### OpenAI JavaScript library
+responses_result = client.responses.create(
+ model='qwen3:8b',
+ input='Write a short poem about the color blue',
+)
+print(responses_result.output_text)
+```
-```javascript
+```javascript responses.js
import OpenAI from "openai";
const openai = new OpenAI({
baseURL: "http://localhost:11434/v1/",
+ apiKey: "ollama", // required but ignored
+});
- // required but ignored
- apiKey: "ollama",
+const responsesResult = await openai.responses.create({
+ model: "qwen3:8b",
+ input: "Write a short poem about the color blue",
});
-const chatCompletion = await openai.chat.completions.create({
- messages: [{ role: "user", content: "Say this is a test" }],
- model: "llama3.2",
+console.log(responsesResult.output_text);
+```
+
+```shell responses.sh
+curl -X POST http://localhost:11434/v1/responses \
+-H "Content-Type: application/json" \
+-d '{
+ "model": "qwen3:8b",
+ "input": "Write a short poem about the color blue"
+}'
+```
+
+
+
+### v1/chat/completions with vision example
+
+
+
+```python vision.py
+from openai import OpenAI
+
+client = OpenAI(
+ base_url='http://localhost:11434/v1/',
+ api_key='ollama', # required but ignored
+)
+
+response = client.chat.completions.create(
+ model='qwen3-vl:8b',
+ messages=[
+ {
+ 'role': 'user',
+ 'content': [
+ {'type': 'text', 'text': "What's in this image?"},
+ {
+ 'type': 'image_url',
+ 'image_url': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAG0AAABmCAYAAADBPx+VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAA3VSURBVHgB7Z27r0zdG8fX743i1bi1ikMoFMQloXRpKFFIqI7LH4BEQ+NWIkjQuSWCRIEoULk0gsK1kCBI0IhrQVT7tz/7zZo888yz1r7MnDl7z5xvsjkzs2fP3uu71nNfa7lkAsm7d++Sffv2JbNmzUqcc8m0adOSzZs3Z+/XES4ZckAWJEGWPiCxjsQNLWmQsWjRIpMseaxcuTKpG/7HP27I8P79e7dq1ars/yL4/v27S0ejqwv+cUOGEGGpKHR37tzJCEpHV9tnT58+dXXCJDdECBE2Ojrqjh071hpNECjx4cMHVycM1Uhbv359B2F79+51586daxN/+pyRkRFXKyRDAqxEp4yMlDDzXG1NPnnyJKkThoK0VFd1ELZu3TrzXKxKfW7dMBQ6bcuWLW2v0VlHjx41z717927ba22U9APcw7Nnz1oGEPeL3m3p2mTAYYnFmMOMXybPPXv2bNIPpFZr1NHn4HMw0KRBjg9NuRw95s8PEcz/6DZELQd/09C9QGq5RsmSRybqkwHGjh07OsJSsYYm3ijPpyHzoiacg35MLdDSIS/O1yM778jOTwYUkKNHWUzUWaOsylE00MyI0fcnOwIdjvtNdW/HZwNLGg+sR1kMepSNJXmIwxBZiG8tDTpEZzKg0GItNsosY8USkxDhD0Rinuiko2gfL/RbiD2LZAjU9zKQJj8RDR0vJBR1/Phx9+PHj9Z7REF4nTZkxzX4LCXHrV271qXkBAPGfP/atWvu/PnzHe4C97F48eIsRLZ9+3a3f/9+87dwP1JxaF7/3r17ba+5l4EcaVo0lj3SBq5kGTJSQmLWMjgYNei2GPT1MuMqGTDEFHzeQSP2wi/jGnkmPJ/nhccs44jvDAxpVcxnq0F6eT8h4ni/iIWpR5lPyA6ETkNXoSukvpJAD3AsXLiwpZs49+fPn5ke4j10TqYvegSfn0OnafC+Tv9ooA/JPkgQysqQNBzagXY55nO/oa1F7qvIPWkRL12WRpMWUvpVDYmxAPehxWSe8ZEXL20sadYIozfmNch4QJPAfeJgW3rNsnzphBKNJM2KKODo1rVOMRYik5ETy3ix4qWNI81qAAirizgMIc+yhTytx0JWZuNI03qsrgWlGtwjoS9XwgUhWGyhUaRZZQNNIEwCiXD16tXcAHUs79co0vSD8rrJCIW98pzvxpAWyyo3HYwqS0+H0BjStClcZJT5coMm6D2LOF8TolGJtK9fvyZpyiC5ePFi9nc/oJU4eiEP0jVoAnHa9wyJycITMP78+eMeP37sXrx44d6+fdt6f82aNdkx1pg9e3Zb5W+RSRE+n+VjksQWifvVaTKFhn5O8my63K8Qabdv33b379/PiAP//vuvW7BggZszZ072/+TJk91YgkafPn166zXB1rQHFvouAWHq9z3SEevSUerqCn2/dDCeta2jxYbr69evk4MHDyY7d+7MjhMnTiTPnz9Pfv/+nfQT2ggpO2dMF8cghuoM7Ygj5iWCqRlGFml0QC/ftGmTmzt3rmsaKDsgBSPh0/8yPeLLBihLkOKJc0jp8H8vUzcxIA1k6QJ/c78tWEyj5P3o4u9+jywNPdJi5rAH9x0KHcl4Hg570eQp3+vHXGyrmEeigzQsQsjavXt38ujRo44LQuDDhw+TW7duRS1HGgMxhNXHgflaNTOsHyKvHK5Ijo2jbFjJBQK9YwFd6RVMzfgRBmEfP37suBBm/p49e1qjEP2mwTViNRo0VJWH1deMXcNK08uUjVUu7s/zRaL+oLNxz1bpANco4npUgX4G2eFbpDFyQoQxojBCpEGSytmOH8qrH5Q9vuzD6ofQylkCUmh8DBAr+q8JCyVNtWQIidKQE9wNtLSQnS4jDSsxNHogzFuQBw4cyM61UKVsjfr3ooBkPSqqQHesUPWVtzi9/vQi1T+rJj7WiTz4Pt/l3LxUkr5P2VYZaZ4URpsE+st/dujQoaBBYokbrz/8TJNQYLSonrPS9kUaSkPeZyj1AWSj+d+VBoy1pIWVNed8P0Ll/ee5HdGRhrHhR5GGN0r4LGZBaj8oFDJitBTJzIZgFcmU0Y8ytWMZMzJOaXUSrUs5RxKnrxmbb5YXO9VGUhtpXldhEUogFr3IzIsvlpmdosVcGVGXFWp2oU9kLFL3dEkSz6NHEY1sjSRdIuDFWEhd8KxFqsRi1uM/nz9/zpxnwlESONdg6dKlbsaMGS4EHFHtjFIDHwKOo46l4TxSuxgDzi+rE2jg+BaFruOX4HXa0Nnf1lwAPufZeF8/r6zD97WK2qFnGjBxTw5qNGPxT+5T/r7/7RawFC3j4vTp09koCxkeHjqbHJqArmH5UrFKKksnxrK7FuRIs8STfBZv+luugXZ2pR/pP9Ois4z+TiMzUUkUjD0iEi1fzX8GmXyuxUBRcaUfykV0YZnlJGKQpOiGB76x5GeWkWWJc3mOrK6S7xdND+W5N6XyaRgtWJFe13GkaZnKOsYqGdOVVVbGupsyA/l7emTLHi7vwTdirNEt0qxnzAvBFcnQF16xh/TMpUuXHDowhlA9vQVraQhkudRdzOnK+04ZSP3DUhVSP61YsaLtd/ks7ZgtPcXqPqEafHkdqa84X6aCeL7YWlv6edGFHb+ZFICPlljHhg0bKuk0CSvVznWsotRu433alNdFrqG45ejoaPCaUkWERpLXjzFL2Rpllp7PJU2a/v7Ab8N05/9t27Z16KUqoFGsxnI9EosS2niSYg9SpU6B4JgTrvVW1flt1sT+0ADIJU2maXzcUTraGCRaL1Wp9rUMk16PMom8QhruxzvZIegJjFU7LLCePfS8uaQdPny4jTTL0dbee5mYokQsXTIWNY46kuMbnt8Kmec+LGWtOVIl9cT1rCB0V8WqkjAsRwta93TbwNYoGKsUSChN44lgBNCoHLHzquYKrU6qZ8lolCIN0Rh6cP0Q3U6I6IXILYOQI513hJaSKAorFpuHXJNfVlpRtmYBk1Su1obZr5dnKAO+L10Hrj3WZW+E3qh6IszE37F6EB+68mGpvKm4eb9bFrlzrok7fvr0Kfv727dvWRmdVTJHw0qiiCUSZ6wCK+7XL/AcsgNyL74DQQ730sv78Su7+t/A36MdY0sW5o40ahslXr58aZ5HtZB8GH64m9EmMZ7FpYw4T6QnrZfgenrhFxaSiSGXtPnz57e9TkNZLvTjeqhr734CNtrK41L40sUQckmj1lGKQ0rC37x544r8eNXRpnVE3ZZY7zXo8NomiO0ZUCj2uHz58rbXoZ6gc0uA+F6ZeKS/jhRDUq8MKrTho9fEkihMmhxtBI1DxKFY9XLpVcSkfoi8JGnToZO5sU5aiDQIW716ddt7ZLYtMQlhECdBGXZZMWldY5BHm5xgAroWj4C0hbYkSc/jBmggIrXJWlZM6pSETsEPGqZOndr2uuuR5rF169a2HoHPdurUKZM4CO1WTPqaDaAd+GFGKdIQkxAn9RuEWcTRyN2KSUgiSgF5aWzPTeA/lN5rZubMmR2bE4SIC4nJoltgAV/dVefZm72AtctUCJU2CMJ327hxY9t7EHbkyJFseq+EJSY16RPo3Dkq1kkr7+q0bNmyDuLQcZBEPYmHVdOBiJyIlrRDq41YPWfXOxUysi5fvtyaj+2BpcnsUV/oSoEMOk2CQGlr4ckhBwaetBhjCwH0ZHtJROPJkyc7UjcYLDjmrH7ADTEBXFfOYmB0k9oYBOjJ8b4aOYSe7QkKcYhFlq3QYLQhSidNmtS2RATwy8YOM3EQJsUjKiaWZ+vZToUQgzhkHXudb/PW5YMHD9yZM2faPsMwoc7RciYJXbGuBqJ1UIGKKLv915jsvgtJxCZDubdXr165mzdvtr1Hz5LONA8jrUwKPqsmVesKa49S3Q4WxmRPUEYdTjgiUcfUwLx589ySJUva3oMkP6IYddq6HMS4o55xBJBUeRjzfa4Zdeg56QZ43LhxoyPo7Lf1kNt7oO8wWAbNwaYjIv5lhyS7kRf96dvm5Jah8vfvX3flyhX35cuX6HfzFHOToS1H4BenCaHvO8pr8iDuwoUL7tevX+b5ZdbBair0xkFIlFDlW4ZknEClsp/TzXyAKVOmmHWFVSbDNw1l1+4f90U6IY/q4V27dpnE9bJ+v87QEydjqx/UamVVPRG+mwkNTYN+9tjkwzEx+atCm/X9WvWtDtAb68Wy9LXa1UmvCDDIpPkyOQ5ZwSzJ4jMrvFcr0rSjOUh+GcT4LSg5ugkW1Io0/SCDQBojh0hPlaJdah+tkVYrnTZowP8iq1F1TgMBBauufyB33x1v+NWFYmT5KmppgHC+NkAgbmRkpD3yn9QIseXymoTQFGQmIOKTxiZIWpvAatenVqRVXf2nTrAWMsPnKrMZHz6bJq5jvce6QK8J1cQNgKxlJapMPdZSR64/UivS9NztpkVEdKcrs5alhhWP9NeqlfWopzhZScI6QxseegZRGeg5a8C3Re1Mfl1ScP36ddcUaMuv24iOJtz7sbUjTS4qBvKmstYJoUauiuD3k5qhyr7QdUHMeCgLa1Ear9NquemdXgmum4fvJ6w1lqsuDhNrg1qSpleJK7K3TF0Q2jSd94uSZ60kK1e3qyVpQK6PVWXp2/FC3mp6jBhKKOiY2h3gtUV64TWM6wDETRPLDfSakXmH3w8g9Jlug8ZtTt4kVF0kLUYYmCCtD/DrQ5YhMGbA9L3ucdjh0y8kOHW5gU/VEEmJTcL4Pz/f7mgoAbYkAAAAAElFTkSuQmCC',
+ },
+ ],
+ }
+ ],
+ max_tokens=300,
+)
+print(response.choices[0].message.content)
+```
+
+```javascript vision.js
+import OpenAI from "openai";
+
+const openai = new OpenAI({
+ baseURL: "http://localhost:11434/v1/",
+ apiKey: "ollama", // required but ignored
});
const response = await openai.chat.completions.create({
- model: "llava",
+ model: "qwen3-vl:8b",
messages: [
{
role: "user",
@@ -129,84 +158,20 @@ const response = await openai.chat.completions.create({
},
],
});
-
-const completion = await openai.completions.create({
- model: "llama3.2",
- prompt: "Say this is a test.",
-});
-
-const listCompletion = await openai.models.list();
-
-const model = await openai.models.retrieve("llama3.2");
-
-const embedding = await openai.embeddings.create({
- model: "all-minilm",
- input: ["why is the sky blue?", "why is the grass green?"],
-});
+console.log(response.choices[0].message.content);
```
-### `curl`
-
-```shell
-curl http://localhost:11434/v1/chat/completions \
- -H "Content-Type: application/json" \
- -d '{
- "model": "llama3.2",
- "messages": [
- {
- "role": "system",
- "content": "You are a helpful assistant."
- },
- {
- "role": "user",
- "content": "Hello!"
- }
- ]
- }'
-
-curl http://localhost:11434/v1/chat/completions \
- -H "Content-Type: application/json" \
- -d '{
- "model": "llava",
- "messages": [
- {
- "role": "user",
- "content": [
- {
- "type": "text",
- "text": "What'\''s in this image?"
- },
- {
- "type": "image_url",
- "image_url": {
- "url": "data:image/png;base64,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"
- }
- }
- ]
- }
- ],
- "max_tokens": 300
- }'
-
-curl http://localhost:11434/v1/completions \
- -H "Content-Type: application/json" \
- -d '{
- "model": "llama3.2",
- "prompt": "Say this is a test"
- }'
-
-curl http://localhost:11434/v1/models
-
-curl http://localhost:11434/v1/models/llama3.2
-
-curl http://localhost:11434/v1/embeddings \
- -H "Content-Type: application/json" \
- -d '{
- "model": "all-minilm",
- "input": ["why is the sky blue?", "why is the grass green?"]
- }'
+```shell vision.sh
+curl -X POST http://localhost:11434/v1/chat/completions \
+-H "Content-Type: application/json" \
+-d '{
+ "model": "qwen3-vl:8b",
+ "messages": [{ "role": "user", "content": [{"type": "text", "text": "What is this an image of?"}, {"type": "image_url", "image_url": "data:image/png;base64,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"}]}]
+}'
```
+
+
## Endpoints
### `/v1/chat/completions`
@@ -310,6 +275,33 @@ curl http://localhost:11434/v1/embeddings \
- [x] `dimensions`
- [ ] `user`
+### `/v1/responses`
+
+> Note: Added in Ollama v0.13.3
+
+Ollama supports the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses). Only the non-stateful flavor is supported (i.e., there is no `previous_response_id` or `conversation` support).
+
+#### Supported features
+
+- [x] Streaming
+- [x] Tools (function calling)
+- [x] Reasoning summaries (for thinking models)
+- [ ] Stateful requests
+
+#### Supported request fields
+
+- [x] `model`
+- [x] `input`
+- [x] `instructions`
+- [x] `tools`
+- [x] `stream`
+- [x] `temperature`
+- [x] `top_p`
+- [x] `max_output_tokens`
+- [ ] `previous_response_id` (stateful v1/responses not supported)
+- [ ] `conversation` (stateful v1/responses not supported)
+- [ ] `truncation`
+
## Models
Before using a model, pull it locally `ollama pull`:
@@ -365,4 +357,4 @@ curl http://localhost:11434/v1/chat/completions \
}
]
}'
-```
\ No newline at end of file
+```
diff --git a/docs/capabilities/tool-calling.mdx b/docs/capabilities/tool-calling.mdx
index ae1ff959942..30c994d9fb8 100644
--- a/docs/capabilities/tool-calling.mdx
+++ b/docs/capabilities/tool-calling.mdx
@@ -15,7 +15,7 @@ Also known as "single-shot" tool calling.
```shell
curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{
"model": "qwen3",
- "messages": [{"role": "user", "content": "What's the temperature in New York?"}],
+ "messages": [{"role": "user", "content": "What is the temperature in New York?"}],
"stream": false,
"tools": [
{
@@ -41,7 +41,7 @@ Also known as "single-shot" tool calling.
curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{
"model": "qwen3",
"messages": [
- {"role": "user", "content": "What's the temperature in New York?"},
+ {"role": "user", "content": "What is the temperature in New York?"},
{
"role": "assistant",
"tool_calls": [
@@ -90,7 +90,7 @@ Also known as "single-shot" tool calling.
}
return temperatures.get(city, "Unknown")
- messages = [{"role": "user", "content": "What's the temperature in New York?"}]
+ messages = [{"role": "user", "content": "What is the temperature in New York?"}]
# pass functions directly as tools in the tools list or as a JSON schema
response = chat(model="qwen3", messages=messages, tools=[get_temperature], think=True)
@@ -146,7 +146,7 @@ Also known as "single-shot" tool calling.
},
]
- const messages = [{ role: 'user', content: "What's the temperature in New York?" }]
+ const messages = [{ role: 'user', content: "What is the temperature in New York?" }]
const response = await ollama.chat({
model: 'qwen3',
@@ -609,7 +609,7 @@ def get_temperature(city: str) -> str:
return temperatures.get(city, 'Unknown')
-messages = [{'role': 'user', 'content': "What's the temperature in New York?"}]
+messages = [{'role': 'user', 'content': "What is the temperature in New York?"}]
while True:
stream = chat(
@@ -684,7 +684,7 @@ const getTemperatureTool = {
}
async function agentLoop() {
- const messages = [{ role: 'user', content: "What's the temperature in New York?" }]
+ const messages = [{ role: 'user', content: "What is the temperature in New York?" }]
while (true) {
const stream = await ollama.chat({
diff --git a/docs/capabilities/vision.mdx b/docs/capabilities/vision.mdx
index 3342eae2587..81fa260e9e6 100644
--- a/docs/capabilities/vision.mdx
+++ b/docs/capabilities/vision.mdx
@@ -36,7 +36,6 @@ Provide an `images` array. SDKs accept file paths, URLs or raw bytes while the R
}],
"stream": false
}'
- "
```
diff --git a/docs/cloud.mdx b/docs/cloud.mdx
index cea27216f2e..4f4c3722b9b 100644
--- a/docs/cloud.mdx
+++ b/docs/cloud.mdx
@@ -9,15 +9,9 @@ sidebarTitle: Cloud
Ollama's cloud models are a new kind of model in Ollama that can run without a powerful GPU. Instead, cloud models are automatically offloaded to Ollama's cloud service while offering the same capabilities as local models, making it possible to keep using your local tools while running larger models that wouldn't fit on a personal computer.
-Ollama currently supports the following cloud models, with more coming soon:
-
-- `deepseek-v3.1:671b-cloud`
-- `gpt-oss:20b-cloud`
-- `gpt-oss:120b-cloud`
-- `kimi-k2:1t-cloud`
-- `qwen3-coder:480b-cloud`
-- `glm-4.6:cloud`
-- `minimax-m2:cloud`
+### Supported models
+
+For a list of supported models, see Ollama's [model library](https://ollama.com/search?c=cloud).
### Running Cloud models
diff --git a/docs/development.md b/docs/development.md
index ff07b5fb6d1..d0120a19183 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -49,6 +49,8 @@ Install prerequisites:
- [Ninja](https://github.com/ninja-build/ninja/releases)
- (Optional) NVIDIA GPU support
- [CUDA SDK](https://developer.nvidia.com/cuda-downloads?target_os=Windows&target_arch=x86_64&target_version=11&target_type=exe_network)
+- (Optional) VULKAN GPU support
+ - [VULKAN SDK](https://vulkan.lunarg.com/sdk/home) - useful for AMD/Intel GPUs
Then, configure and build the project:
@@ -57,6 +59,17 @@ cmake -B build
cmake --build build --config Release
```
+> Building for Vulkan requires VULKAN_SDK environment variable:
+>
+> PowerShell
+> ```powershell
+> $env:VULKAN_SDK="C:\VulkanSDK\"
+> ```
+> CMD
+> ```cmd
+> set VULKAN_SDK=C:\VulkanSDK\
+> ```
+
> [!IMPORTANT]
> Building for ROCm requires additional flags:
> ```
@@ -65,6 +78,7 @@ cmake --build build --config Release
> ```
+
Lastly, run Ollama:
```shell
@@ -84,7 +98,9 @@ Install prerequisites:
- [ROCm](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/install/quick-start.html)
- (Optional) NVIDIA GPU support
- [CUDA SDK](https://developer.nvidia.com/cuda-downloads)
-
+- (Optional) VULKAN GPU support
+ - [VULKAN SDK](https://vulkan.lunarg.com/sdk/home) - useful for AMD/Intel GPUs
+ - Or install via package manager: `sudo apt install vulkan-sdk` (Ubuntu/Debian) or `sudo dnf install vulkan-sdk` (Fedora/CentOS)
> [!IMPORTANT]
> Ensure prerequisites are in `PATH` before running CMake.
diff --git a/docs/faq.mdx b/docs/faq.mdx
index d9398e9d4be..4237da41f49 100644
--- a/docs/faq.mdx
+++ b/docs/faq.mdx
@@ -14,11 +14,11 @@ curl -fsSL https://ollama.com/install.sh | sh
## How can I view the logs?
-Review the [Troubleshooting](./troubleshooting.md) docs for more about using logs.
+Review the [Troubleshooting](./troubleshooting) docs for more about using logs.
## Is my GPU compatible with Ollama?
-Please refer to the [GPU docs](./gpu.md).
+Please refer to the [GPU docs](./gpu).
## How can I specify the context window size?
@@ -57,8 +57,13 @@ ollama ps
```
- **Output**: ``` NAME ID SIZE PROCESSOR UNTIL llama3:70b bcfb190ca3a7 42 GB
- 100% GPU 4 minutes from now ```
+
+**Output**:
+
+```
+NAME ID SIZE PROCESSOR UNTIL
+llama3:70b bcfb190ca3a7 42 GB 100% GPU 4 minutes from now
+```
The `Processor` column will show which memory the model was loaded in to:
@@ -385,4 +390,4 @@ Ollama for Windows and macOS register as a login item during installation. You
- In `Task Manager` go to the `Startup apps` tab, search for `ollama` then click `Disable`
**MacOS**
-- Open `Settings` and search for "Login Items", find the `Ollama` entry under "Allow in the Background`, then click the slider to disable.
\ No newline at end of file
+- Open `Settings` and search for "Login Items", find the `Ollama` entry under "Allow in the Background`, then click the slider to disable.
diff --git a/docs/gpu.mdx b/docs/gpu.mdx
index 36bfd3da7cd..9cb2d3abc53 100644
--- a/docs/gpu.mdx
+++ b/docs/gpu.mdx
@@ -33,7 +33,7 @@ Check your compute compatibility to see if your card is supported:
| 5.0 | GeForce GTX | `GTX 750 Ti` `GTX 750` `NVS 810` |
| | Quadro | `K2200` `K1200` `K620` `M1200` `M520` `M5000M` `M4000M` `M3000M` `M2000M` `M1000M` `K620M` `M600M` `M500M` |
-For building locally to support older GPUs, see [developer.md](./development.md#linux-cuda-nvidia)
+For building locally to support older GPUs, see [developer](./development#linux-cuda-nvidia)
### GPU Selection
@@ -54,7 +54,7 @@ sudo modprobe nvidia_uvm`
Ollama supports the following AMD GPUs via the ROCm library:
-> [!NOTE]
+> **NOTE:**
> Additional AMD GPU support is provided by the Vulkan Library - see below.
@@ -132,9 +132,9 @@ Ollama supports GPU acceleration on Apple devices via the Metal API.
## Vulkan GPU Support
-> [!NOTE]
+> **NOTE:**
> Vulkan is currently an Experimental feature. To enable, you must set OLLAMA_VULKAN=1 for the Ollama server as
-described in the [FAQ](faq.md#how-do-i-configure-ollama-server)
+described in the [FAQ](faq#how-do-i-configure-ollama-server)
Additional GPU support on Windows and Linux is provided via
[Vulkan](https://www.vulkan.org/). On Windows most GPU vendors drivers come
@@ -161,6 +161,6 @@ sudo setcap cap_perfmon+ep /usr/local/bin/ollama
To select specific Vulkan GPU(s), you can set the environment variable
`GGML_VK_VISIBLE_DEVICES` to one or more numeric IDs on the Ollama server as
-described in the [FAQ](faq.md#how-do-i-configure-ollama-server). If you
+described in the [FAQ](faq#how-do-i-configure-ollama-server). If you
encounter any problems with Vulkan based GPUs, you can disable all Vulkan GPUs
by setting `GGML_VK_VISIBLE_DEVICES=-1`
\ No newline at end of file
diff --git a/docs/import.mdx b/docs/import.mdx
index b19596894b2..26a6365640c 100644
--- a/docs/import.mdx
+++ b/docs/import.mdx
@@ -88,6 +88,10 @@ To import a GGUF model, create a `Modelfile` containing:
```dockerfile
FROM /path/to/file.gguf
```
+Or:
+```dockerfile
+FROM /path/to/gguf/split/directory
+```
For a GGUF adapter, create the `Modelfile` with:
diff --git a/docs/integrations/vscode.mdx b/docs/integrations/vscode.mdx
index 6f407a88b72..c04059b6f90 100644
--- a/docs/integrations/vscode.mdx
+++ b/docs/integrations/vscode.mdx
@@ -1,34 +1,34 @@
---
-title: VS Code
+title: VS Code
---
## Install
-Install [VS Code](https://code.visualstudio.com/download).
+Install [VS Code](https://code.visualstudio.com/download).
-## Usage with Ollama
+## Usage with Ollama
1. Open Copilot side bar found in top right window
-
-

-
-2. Select the model drowpdown > **Manage models**
-
-

-
+
+

+
+2. Select the model dropdown > **Manage models**
+
+

+
3. Enter **Ollama** under **Provider Dropdown** and select desired models (e.g `qwen3, qwen3-coder:480b-cloud`)
-
-

-
+
+

+
diff --git a/docs/modelfile.mdx b/docs/modelfile.mdx
index c91d7310ce3..0a0ca47ecc0 100644
--- a/docs/modelfile.mdx
+++ b/docs/modelfile.mdx
@@ -12,7 +12,7 @@ A Modelfile is the blueprint to create and share customized models using Ollama.
- [FROM (Required)](#from-required)
- [Build from existing model](#build-from-existing-model)
- [Build from a Safetensors model](#build-from-a-safetensors-model)
- - [Build from a GGUF file](#build-from-a-gguf-file)
+ - [Build from a GGUF file](#build-from-a-gguf-model)
- [PARAMETER](#parameter)
- [Valid Parameters and Values](#valid-parameters-and-values)
- [TEMPLATE](#template)
@@ -41,6 +41,7 @@ INSTRUCTION arguments
| [`ADAPTER`](#adapter) | Defines the (Q)LoRA adapters to apply to the model. |
| [`LICENSE`](#license) | Specifies the legal license. |
| [`MESSAGE`](#message) | Specify message history. |
+| [`REQUIRES`](#requires) | Specify the minimum version of Ollama required by the model. |
## Examples
@@ -129,7 +130,7 @@ Currently supported model architectures:
- Gemma (including Gemma 1 and Gemma 2)
- Phi3
-#### Build from a GGUF file
+#### Build from a GGUF model
```
FROM ./ollama-model.gguf
@@ -137,6 +138,14 @@ FROM ./ollama-model.gguf
The GGUF file location should be specified as an absolute path or relative to the `Modelfile` location.
+For GGUF model split into multiple files:
+
+```
+FROM
+```
+
+The model directory should contain solely the split GGUF weights of one model.
+
### PARAMETER
The `PARAMETER` instruction defines a parameter that can be set when the model is run.
@@ -149,9 +158,6 @@ PARAMETER
| Parameter | Description | Value Type | Example Usage |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------- |
-| mirostat | Enable Mirostat sampling for controlling perplexity. (default: 0, 0 = disabled, 1 = Mirostat, 2 = Mirostat 2.0) | int | mirostat 0 |
-| mirostat_eta | Influences how quickly the algorithm responds to feedback from the generated text. A lower learning rate will result in slower adjustments, while a higher learning rate will make the algorithm more responsive. (Default: 0.1) | float | mirostat_eta 0.1 |
-| mirostat_tau | Controls the balance between coherence and diversity of the output. A lower value will result in more focused and coherent text. (Default: 5.0) | float | mirostat_tau 5.0 |
| num_ctx | Sets the size of the context window used to generate the next token. (Default: 2048) | int | num_ctx 4096 |
| repeat_last_n | Sets how far back for the model to look back to prevent repetition. (Default: 64, 0 = disabled, -1 = num_ctx) | int | repeat_last_n 64 |
| repeat_penalty | Sets how strongly to penalize repetitions. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. (Default: 1.1) | float | repeat_penalty 1.1 |
@@ -251,6 +257,16 @@ MESSAGE user Is Ontario in Canada?
MESSAGE assistant yes
```
+### REQUIRES
+
+The `REQUIRES` instruction allows you to specify the minimum version of Ollama required by the model.
+
+```
+REQUIRES
+```
+
+The version should be a valid Ollama version (e.g. 0.14.0).
+
## Notes
- the **`Modelfile` is not case sensitive**. In the examples, uppercase instructions are used to make it easier to distinguish it from arguments.
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index 61f2a6fa3a8..4817bcb41f5 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -111,6 +111,12 @@ components:
description: Model keep-alive duration (for example `5m` or `0` to unload immediately)
options:
$ref: "#/components/schemas/ModelOptions"
+ logprobs:
+ type: boolean
+ description: Whether to return log probabilities of the output tokens
+ top_logprobs:
+ type: integer
+ description: Number of most likely tokens to return at each token position when logprobs are enabled
GenerateResponse:
type: object
properties:
@@ -150,6 +156,11 @@ components:
eval_duration:
type: integer
description: Time spent generating tokens in nanoseconds
+ logprobs:
+ type: array
+ items:
+ $ref: "#/components/schemas/Logprob"
+ description: Log probability information for the generated tokens when logprobs are enabled
GenerateStreamEvent:
type: object
properties:
@@ -287,6 +298,12 @@ components:
- type: string
- type: number
description: Model keep-alive duration (for example `5m` or `0` to unload immediately)
+ logprobs:
+ type: boolean
+ description: Whether to return log probabilities of the output tokens
+ top_logprobs:
+ type: integer
+ description: Number of most likely tokens to return at each token position when logprobs are enabled
ChatResponse:
type: object
properties:
@@ -344,6 +361,11 @@ components:
eval_duration:
type: integer
description: Time spent generating tokens in nanoseconds
+ logprobs:
+ type: array
+ items:
+ $ref: "#/components/schemas/Logprob"
+ description: Log probability information for the generated tokens when logprobs are enabled
ChatStreamEvent:
type: object
properties:
@@ -706,6 +728,41 @@ components:
version:
type: string
description: Version of Ollama
+ TokenLogprob:
+ type: object
+ description: Log probability information for a single token alternative
+ properties:
+ token:
+ type: string
+ description: The text representation of the token
+ logprob:
+ type: number
+ description: The log probability of this token
+ bytes:
+ type: array
+ items:
+ type: integer
+ description: The raw byte representation of the token
+ Logprob:
+ type: object
+ description: Log probability information for a generated token
+ properties:
+ token:
+ type: string
+ description: The text representation of the token
+ logprob:
+ type: number
+ description: The log probability of this token
+ bytes:
+ type: array
+ items:
+ type: integer
+ description: The raw byte representation of the token
+ top_logprobs:
+ type: array
+ items:
+ $ref: "#/components/schemas/TokenLogprob"
+ description: Most likely tokens and their log probabilities at this position
ErrorResponse:
type: object
properties:
diff --git a/docs/tools/extract-examples/README.md b/docs/tools/extract-examples/README.md
new file mode 100644
index 00000000000..385604921ec
--- /dev/null
+++ b/docs/tools/extract-examples/README.md
@@ -0,0 +1,46 @@
+# extract-examples
+
+Extracts code examples from MDX files to a temp directory so you can run them.
+
+## Usage
+
+```shell
+go run docs/tools/extract-examples/main.go
+```
+
+## Example
+
+```shell
+go run docs/tools/extract-examples/main.go docs/api/openai-compatibility.mdx
+```
+
+Output:
+
+```
+Extracting code examples to: /var/folders/vq/wfm2g6k917d3ldzpjdxc8ph00000gn/T/mdx-examples-3271754368
+
+ - 01_basic.py
+ - 01_basic.js
+ - 01_basic.sh
+ - 02_responses.py
+ - 02_responses.js
+ - 02_responses.sh
+ - 03_vision.py
+ - 03_vision.js
+ - 03_vision.sh
+
+Extracted 9 file(s) to /var/folders/vq/wfm2g6k917d3ldzpjdxc8ph00000gn/T/mdx-examples-3271754368
+
+To run examples:
+
+ cd /var/folders/vq/wfm2g6k917d3ldzpjdxc8ph00000gn/T/mdx-examples-3271754368
+ npm install # for JS examples
+
+then run individual files with `node file.js`, `python file.py`, `bash file.sh`
+```
+
+## How it works
+
+- Parses MDX files looking for fenced code blocks with filenames (e.g., ` ```python basic.py `)
+- Groups examples by their `` and prefixes filenames with `01_`, `02_`, etc.
+- Writes all extracted files to a temp directory
diff --git a/docs/tools/extract-examples/main.go b/docs/tools/extract-examples/main.go
new file mode 100644
index 00000000000..3f09af5ce8a
--- /dev/null
+++ b/docs/tools/extract-examples/main.go
@@ -0,0 +1,137 @@
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+func main() {
+ if len(os.Args) < 2 {
+ fmt.Fprintln(os.Stderr, "Usage: go run extract-examples.go ")
+ os.Exit(1)
+ }
+
+ mdxFile := os.Args[1]
+
+ f, err := os.Open(mdxFile)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ os.Exit(1)
+ }
+ defer f.Close()
+
+ // Create temp directory
+ tempDir, err := os.MkdirTemp("", "mdx-examples-*")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error creating temp dir: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Printf("Extracting code examples to: %s\n\n", tempDir)
+
+ // Patterns
+ codeBlockStart := regexp.MustCompile("^```([a-zA-Z0-9_-]+)\\s+([^\\s]+)$")
+ codeGroupStart := regexp.MustCompile("^")
+
+ scanner := bufio.NewScanner(f)
+ inCodeBlock := false
+ inCodeGroup := false
+ var currentFile string
+ var content strings.Builder
+ count := 0
+ codeGroupNum := 0
+
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ // Track CodeGroup boundaries
+ if codeGroupStart.MatchString(line) {
+ inCodeGroup = true
+ codeGroupNum++
+ continue
+ }
+ if codeGroupEnd.MatchString(line) {
+ inCodeGroup = false
+ continue
+ }
+
+ if inCodeBlock {
+ if line == "```" {
+ // End of code block - write file
+ if currentFile != "" {
+ outPath := filepath.Join(tempDir, currentFile)
+ if err := os.WriteFile(outPath, []byte(content.String()), 0o644); err != nil {
+ fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", currentFile, err)
+ } else {
+ fmt.Printf(" - %s\n", currentFile)
+ count++
+ }
+ }
+ inCodeBlock = false
+ currentFile = ""
+ content.Reset()
+ } else {
+ content.WriteString(line)
+ content.WriteString("\n")
+ }
+ } else {
+ if matches := codeBlockStart.FindStringSubmatch(line); matches != nil {
+ inCodeBlock = true
+ filename := matches[2]
+ // Prefix with CodeGroup number if inside a CodeGroup
+ if inCodeGroup {
+ currentFile = fmt.Sprintf("%02d_%s", codeGroupNum, filename)
+ } else {
+ currentFile = filename
+ }
+ content.Reset()
+ }
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Write package.json for JavaScript dependencies
+ packageJSON := `{
+ "name": "mdx-examples",
+ "type": "module",
+ "dependencies": {
+ "openai": "^4",
+ "ollama": "^0.5"
+ }
+}
+`
+ if err := os.WriteFile(filepath.Join(tempDir, "package.json"), []byte(packageJSON), 0o644); err != nil {
+ fmt.Fprintf(os.Stderr, "Error writing package.json: %v\n", err)
+ }
+
+ // Write pyproject.toml for Python dependencies
+ pyprojectTOML := `[project]
+name = "mdx-examples"
+version = "0.0.0"
+dependencies = [
+ "openai",
+ "ollama",
+]
+`
+ if err := os.WriteFile(filepath.Join(tempDir, "pyproject.toml"), []byte(pyprojectTOML), 0o644); err != nil {
+ fmt.Fprintf(os.Stderr, "Error writing pyproject.toml: %v\n", err)
+ }
+
+ fmt.Printf("\n")
+ fmt.Printf("Extracted %d file(s) to %s\n", count, tempDir)
+ fmt.Printf("\n")
+ fmt.Printf("To run examples:\n")
+ fmt.Printf("\n")
+ fmt.Printf(" cd %s\n npm install # for JS examples\n", tempDir)
+ fmt.Printf("\n")
+ fmt.Printf("then run individual files with `node file.js`, `python file.py`, `bash file.sh`\n")
+}
diff --git a/docs/troubleshooting.mdx b/docs/troubleshooting.mdx
index ec662572ae9..9083dd8c8b6 100644
--- a/docs/troubleshooting.mdx
+++ b/docs/troubleshooting.mdx
@@ -87,7 +87,7 @@ When Ollama starts up, it takes inventory of the GPUs present in the system to d
### Linux NVIDIA Troubleshooting
-If you are using a container to run Ollama, make sure you've set up the container runtime first as described in [docker.md](./docker.md)
+If you are using a container to run Ollama, make sure you've set up the container runtime first as described in [docker](./docker)
Sometimes the Ollama can have difficulties initializing the GPU. When you check the server logs, this can show up as various error codes, such as "3" (not initialized), "46" (device unavailable), "100" (no device), "999" (unknown), or others. The following troubleshooting techniques may help resolve the problem
diff --git a/fs/ggml/ggml.go b/fs/ggml/ggml.go
index 0b5d37a7bd3..1a1dc29b0ff 100644
--- a/fs/ggml/ggml.go
+++ b/fs/ggml/ggml.go
@@ -7,12 +7,14 @@ import (
"fmt"
"io"
"log/slog"
+ "maps"
"math"
"slices"
"strings"
"github.com/ollama/ollama/format"
"github.com/ollama/ollama/fs/util/bufioutil"
+ "github.com/ollama/ollama/ml"
)
type GGML struct {
@@ -26,6 +28,18 @@ type model interface {
Tensors() Tensors
}
+type MetaGGML struct {
+ Shards []GGML
+ ShardPaths []string
+ Tensors ForeignTensors
+ kv KV
+}
+
+type GGUFSplitInfo struct {
+ No uint16
+ Count uint16
+}
+
type KV map[string]any
func (kv KV) Architecture() string {
@@ -49,6 +63,18 @@ func (kv KV) FileType() FileType {
return FileTypeUnknown
}
+func (kv KV) GGUFSplitInfo() *GGUFSplitInfo {
+ no, found := keyValue(kv, "split.no", uint16(0))
+ if !found {
+ return nil
+ }
+ count, _ := keyValue(kv, "split.count", uint16(0))
+ return &GGUFSplitInfo{
+ No: no,
+ Count: count,
+ }
+}
+
func (kv KV) BlockCount() uint64 {
return uint64(kv.Uint("block_count"))
}
@@ -240,12 +266,17 @@ func (kv KV) Bools(key string, defaultValue ...[]bool) []bool {
func (kv KV) OllamaEngineRequired() bool {
return slices.Contains([]string{
+ "bert",
+ "deepseek2",
+ "deepseekocr",
"gemma3",
"gemma3n",
"gptoss", "gpt-oss",
"llama4",
"mistral3",
"mllama",
+ "nomic-bert",
+ "olmo3",
"qwen25vl",
"qwen3", "qwen3moe",
"qwen3vl", "qwen3vlmoe",
@@ -265,7 +296,7 @@ type arrayValueTypes interface {
}
func keyValue[T valueTypes | arrayValueTypes](kv KV, key string, defaultValue ...T) (T, bool) {
- if !strings.HasPrefix(key, "tokenizer.") && !strings.HasPrefix(key, "general.") {
+ if !strings.HasPrefix(key, "tokenizer.") && !strings.HasPrefix(key, "general.") && !strings.HasPrefix(key, "split.") {
key = kv.Architecture() + "." + key
}
@@ -282,6 +313,14 @@ type Tensors struct {
Offset uint64
}
+type ForeignTensor struct {
+ *Tensor
+ ModelPath string
+ TensorRegionOffset uint64
+}
+
+type ForeignTensors []ForeignTensor
+
func (s Tensors) Items(prefix ...string) []*Tensor {
if len(prefix) == 0 {
return s.items
@@ -320,6 +359,41 @@ func (ts Tensors) GroupLayers() map[string]Layer {
return layers
}
+func (s ForeignTensors) Items(prefix ...string) []*Tensor {
+ var items []*Tensor
+ for i := range s {
+ if len(prefix) == 0 || strings.HasPrefix(s[i].Name, prefix[0]) {
+ items = append(items, s[i].Tensor)
+ }
+ }
+
+ return items
+}
+
+func (ts ForeignTensors) GroupLayers() map[string]Layer {
+ layers := make(map[string]Layer)
+ for i := range ts {
+ t := ts[i].Tensor
+ parts := strings.Split(t.Name, ".")
+ if index := slices.IndexFunc(parts, func(s string) bool { return s == "blk" || s == "mm" }); index != -1 {
+ if len(parts) > index+2 {
+ // blk and mm should have a number after them, join it
+ parts = append(
+ []string{strings.Join(parts[:index+2], ".")},
+ parts[index+2:]...)
+ }
+ }
+
+ if _, ok := layers[parts[0]]; !ok {
+ layers[parts[0]] = make(Layer)
+ }
+
+ layers[parts[0]][strings.Join(parts[1:], ".")] = t
+ }
+
+ return layers
+}
+
type Layer map[string]*Tensor
func (l Layer) Size() (size uint64) {
@@ -547,7 +621,93 @@ func Decode(rs io.ReadSeeker, maxArraySize int) (*GGML, error) {
}, nil
}
-func (f GGML) GraphSize(context, batch uint64, numParallel int, kvCacheType string, useFlashAttention bool) (kv []uint64, partialOffload, fullOffload uint64) {
+func BuildForeignTensors(shards []GGML, shardsPaths []string) (*ForeignTensors, error) {
+ if len(shards) != len(shardsPaths) {
+ return nil, fmt.Errorf("length of shards and shardsPaths do not match: %d vs %d", len(shards), len(shardsPaths))
+ }
+ li := make(ForeignTensors, 0)
+ for i := range shards {
+ gs := shards[i]
+ tensors := gs.Tensors()
+ for k := range tensors.items {
+ tensor := tensors.items[k]
+ li = append(li, ForeignTensor{
+ Tensor: tensor,
+ ModelPath: shardsPaths[i],
+ TensorRegionOffset: tensors.Offset,
+ })
+ }
+ }
+ return &li, nil
+}
+
+func MakeMetaGGML(ggmls []GGML, ggmlPaths []string) MetaGGML {
+ type wrapper struct {
+ ggml GGML
+ path string
+ weight int
+ }
+ var wrappers []wrapper
+ for i := range ggmls {
+ iSplitInfo := ggmls[i].KV().GGUFSplitInfo()
+ var weight int = 0
+ if iSplitInfo == nil {
+ weight = -1
+ } else {
+ weight = int((*iSplitInfo).No)
+ }
+ wrappers = append(wrappers, wrapper{
+ ggml: ggmls[i],
+ path: ggmlPaths[i],
+ weight: weight,
+ })
+ }
+ slices.SortStableFunc(wrappers, func(a, b wrapper) int {
+ return cmp.Compare(a.weight, b.weight)
+ })
+ metaGgml := MetaGGML{}
+ var param_counts uint64 = 0
+ for i := range wrappers {
+ param_counts += wrappers[i].ggml.KV().ParameterCount()
+ if i == 0 {
+ kv := maps.Clone(wrappers[i].ggml.KV())
+ // remove the keys contained in split gguf files. add more if needed.
+ delete(kv, "slice.no")
+ delete(kv, "slice.count")
+ delete(kv, "slice.tensors.count")
+ delete(kv, "general.parameter_count")
+ metaGgml.kv = kv
+ }
+ metaGgml.Shards = append(metaGgml.Shards, wrappers[i].ggml)
+ metaGgml.ShardPaths = append(metaGgml.ShardPaths, wrappers[i].path)
+ }
+ metaGgml.kv["general.parameter_count"] = param_counts
+ ft, _ := BuildForeignTensors(metaGgml.Shards, metaGgml.ShardPaths)
+ metaGgml.Tensors = *ft
+ return metaGgml
+}
+
+func simpleWrapGGML(ggml GGML) MetaGGML {
+ // simply wrap single GGML, without creating foreign tensors
+ return MetaGGML{
+ Shards: []GGML{ggml},
+ ShardPaths: []string{""},
+ kv: ggml.KV(),
+ }
+}
+
+func WrapGGML(ggml GGML) MetaGGML {
+ metaggml := simpleWrapGGML(ggml)
+ ft, _ := BuildForeignTensors(metaggml.Shards, metaggml.ShardPaths)
+ metaggml.Tensors = *ft
+ return metaggml
+}
+
+func (f GGML) GraphSize(context, batch uint64, numParallel int, kvCacheType string, useFlashAttention ml.FlashAttentionType) (kv []uint64, partialOffload, fullOffload uint64) {
+ return WrapGGML(f).GraphSize(context, batch, numParallel, kvCacheType, useFlashAttention)
+}
+
+func (f MetaGGML) GraphSize(context, batch uint64, numParallel int, kvCacheType string, useFlashAttention ml.FlashAttentionType) (kv []uint64, partialOffload, fullOffload uint64) {
context *= uint64(numParallel)
embedding := f.KV().EmbeddingLength()
@@ -561,7 +721,7 @@ func (f GGML) GraphSize(context, batch uint64, numParallel int, kvCacheType stri
embeddingHeadsK := f.KV().EmbeddingHeadCountK()
embeddingHeadsV := f.KV().EmbeddingHeadCountV()
- layers := f.Tensors().GroupLayers()
+ layers := f.Tensors.GroupLayers()
bytesPerElement := kvCacheBytesPerElement(kvCacheType)
@@ -659,7 +819,7 @@ func (f GGML) GraphSize(context, batch uint64, numParallel int, kvCacheType stri
)
var ropeFreqsCount uint64
- if ropeFreqs, ok := f.Tensors().GroupLayers()["rope_freqs"]; ok {
+ if ropeFreqs, ok := f.Tensors.GroupLayers()["rope_freqs"]; ok {
if ropeFreqsWeights, ok := ropeFreqs["weights"]; ok {
ropeFreqsCount = ropeFreqsWeights.Elements()
}
@@ -788,7 +948,7 @@ func (f GGML) GraphSize(context, batch uint64, numParallel int, kvCacheType stri
}
partialOffload = 2 * f.KV().HeadCountMax() / cmp.Or(f.KV().HeadCountKVMin(), 1) * kvTotal / 6
- if useFlashAttention {
+ if useFlashAttention == ml.FlashAttentionEnabled {
// rough estimate of graph size with flash attention on
partialOffload = (4*uint64(numParallel) + context>>10 + 110) * format.MebiByte
}
@@ -799,6 +959,9 @@ func (f GGML) GraphSize(context, batch uint64, numParallel int, kvCacheType stri
// SupportsKVCacheType checks if the requested cache type is supported
func (f GGML) SupportsKVCacheType(cacheType string) bool {
+ return simpleWrapGGML(f).SupportsKVCacheType(cacheType)
+}
+func (f MetaGGML) SupportsKVCacheType(cacheType string) bool {
if cacheType == "" || cacheType == "f16" {
return true
}
@@ -806,8 +969,27 @@ func (f GGML) SupportsKVCacheType(cacheType string) bool {
return slices.Contains([]string{"q8_0", "q4_0"}, cacheType)
}
+// KVCacheTypeIsQuantized checks if the requested cache type is a quantized type
+func (f GGML) KVCacheTypeIsQuantized(cacheType string) bool {
+ if cacheType == "" || cacheType == "f16" || cacheType == "f32" || cacheType == "bf16" {
+ return false
+ }
+ return true
+}
+
+func (f MetaGGML) KVCacheTypeIsQuantized(cacheType string) bool {
+ if cacheType == "" || cacheType == "f16" || cacheType == "f32" || cacheType == "bf16" {
+ return false
+ }
+ return true
+}
+
// SupportsFlashAttention checks if the model supports flash attention
func (f GGML) SupportsFlashAttention() bool {
+ return simpleWrapGGML(f).SupportsFlashAttention()
+}
+
+func (f MetaGGML) SupportsFlashAttention() bool {
_, isEmbedding := f.KV()[fmt.Sprintf("%s.pooling_type", f.KV().Architecture())]
if isEmbedding {
return false
@@ -825,9 +1007,16 @@ func (f GGML) SupportsFlashAttention() bool {
// FlashAttention checks if the model should enable flash attention
func (f GGML) FlashAttention() bool {
+ return simpleWrapGGML(f).FlashAttention()
+}
+
+func (f MetaGGML) FlashAttention() bool {
return slices.Contains([]string{
+ "bert",
"gemma3",
"gptoss", "gpt-oss",
+ "mistral3",
+ "olmo3",
"qwen3", "qwen3moe",
"qwen3vl", "qwen3vlmoe",
}, f.KV().String("general.architecture"))
@@ -846,3 +1035,15 @@ func kvCacheBytesPerElement(cacheType string) float64 {
return 2 // f16 (default)
}
}
+
+func (f MetaGGML) KV() KV {
+ return f.kv
+}
+
+func (f MetaGGML) TotalTensorBytes() uint64 {
+ totalBytes := uint64(0)
+ for i := range f.Shards {
+ totalBytes += uint64(f.Shards[i].Length) - f.Shards[i].Tensors().Offset
+ }
+ return totalBytes
+}
diff --git a/fs/ggml/gguf.go b/fs/ggml/gguf.go
index 9ee42e368d6..b88ec9597d0 100644
--- a/fs/ggml/gguf.go
+++ b/fs/ggml/gguf.go
@@ -305,7 +305,7 @@ func readGGUFV1StringsData(llm *gguf, r io.Reader, a *array[string]) (any, error
a.values[i] = e
} else {
- discardGGUFString(llm, r)
+ _ = discardGGUFString(llm, r)
}
}
@@ -568,7 +568,6 @@ func WriteGGUF(f *os.File, kv KV, ts []*Tensor) error {
g.SetLimit(runtime.GOMAXPROCS(0))
// TODO consider reducing if tensors size * gomaxprocs is larger than free memory
for _, t := range ts {
- t := t
w := io.NewOffsetWriter(f, offset+int64(t.Offset))
g.Go(func() error {
_, err := t.WriteTo(w)
@@ -583,7 +582,8 @@ func ggufWriteKV(ws io.WriteSeeker, arch, k string, v any) error {
if !strings.HasPrefix(k, arch+".") &&
!strings.HasPrefix(k, "general.") &&
!strings.HasPrefix(k, "adapter.") &&
- !strings.HasPrefix(k, "tokenizer.") {
+ !strings.HasPrefix(k, "tokenizer.") &&
+ !strings.HasPrefix(k, "split.") {
k = arch + "." + k
}
@@ -598,6 +598,12 @@ func ggufWriteKV(ws io.WriteSeeker, arch, k string, v any) error {
var err error
switch v := v.(type) {
+ case uint16:
+ err = writeGGUF(ws, ggufTypeUint16, v)
+ case int32:
+ err = writeGGUF(ws, ggufTypeInt32, v)
+ case int64:
+ err = writeGGUF(ws, ggufTypeInt64, v)
case uint32, FileType:
err = writeGGUF(ws, ggufTypeUint32, v)
case uint64:
@@ -612,6 +618,10 @@ func ggufWriteKV(ws io.WriteSeeker, arch, k string, v any) error {
err = writeGGUFArray(ws, ggufTypeInt32, v)
case *array[int32]:
err = writeGGUFArray(ws, ggufTypeInt32, v.values)
+ case []int64:
+ err = writeGGUFArray(ws, ggufTypeInt64, v)
+ case *array[int64]:
+ err = writeGGUFArray(ws, ggufTypeInt64, v.values)
case []uint32:
err = writeGGUFArray(ws, ggufTypeUint32, v)
case *array[uint32]:
diff --git a/fs/ggml/gguf_test.go b/fs/ggml/gguf_test.go
index f0c2826c3ee..51430e3b734 100644
--- a/fs/ggml/gguf_test.go
+++ b/fs/ggml/gguf_test.go
@@ -42,6 +42,10 @@ func TestWriteGGUF(t *testing.T) {
"general.architecture": "test",
"general.alignment": uint32(16),
"test.key": "value",
+ "test.int32_key": int32(-42),
+ "test.int64_key": int64(-9223372036854775808),
+ "test.int32_array": []int32{-1, 0, 1, 2147483647, -2147483648},
+ "test.int64_array": []int64{-1, 0, 1, 9223372036854775807, -9223372036854775808},
"attention.key": "value2",
"tokenizer.key": "value3",
"adapter.key": "value4",
@@ -55,7 +59,7 @@ func TestWriteGGUF(t *testing.T) {
}
defer r.Close()
- ff, err := Decode(r, 0)
+ ff, err := Decode(r, -1)
if err != nil {
t.Fatal(err)
}
@@ -65,15 +69,19 @@ func TestWriteGGUF(t *testing.T) {
"general.alignment": uint32(16),
"general.parameter_count": uint64(54),
"test.key": "value",
+ "test.int32_key": int32(-42),
+ "test.int64_key": int64(-9223372036854775808),
+ "test.int32_array": &array[int32]{size: 5, values: []int32{-1, 0, 1, 2147483647, -2147483648}},
+ "test.int64_array": &array[int64]{size: 5, values: []int64{-1, 0, 1, 9223372036854775807, -9223372036854775808}},
"test.attention.key": "value2",
"tokenizer.key": "value3",
"adapter.key": "value4",
- }, ff.KV()); diff != "" {
+ }, ff.KV(), cmp.AllowUnexported(array[int32]{}, array[int64]{})); diff != "" {
t.Errorf("Mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(Tensors{
- Offset: 800,
+ Offset: 992,
items: []*Tensor{
{Name: "blk.0.attn_k.weight", Offset: 0, Shape: []uint64{2, 3}},
{Name: "blk.0.attn_norm.weight", Offset: 32, Shape: []uint64{2, 3}},
diff --git a/go.mod b/go.mod
index 54e7942cd32..0f7bca5f29f 100644
--- a/go.mod
+++ b/go.mod
@@ -15,9 +15,8 @@ require (
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.9.0
github.com/x448/float16 v0.8.4
- golang.org/x/sync v0.12.0
- golang.org/x/sys v0.36.0
-
+ golang.org/x/sync v0.17.0
+ golang.org/x/sys v0.37.0
)
require (
@@ -29,13 +28,17 @@ require (
github.com/nlpodyssey/gopickle v0.3.0
github.com/pdevine/tensor v0.0.0-20240510204454-f88f4562727c
github.com/tkrajina/typescriptify-golang-structs v0.2.0
+ github.com/wk8/go-ordered-map/v2 v2.1.8
golang.org/x/image v0.22.0
- golang.org/x/tools v0.30.0
+ golang.org/x/mod v0.30.0
+ golang.org/x/tools v0.38.0
gonum.org/v1/gonum v0.15.0
)
require (
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect
+ github.com/bahlo/generic-list-go v0.2.0 // indirect
+ github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/chewxy/hm v1.0.0 // indirect
github.com/chewxy/math32 v1.11.0 // indirect
@@ -45,6 +48,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/flatbuffers v24.3.25+incompatible // indirect
github.com/kr/text v0.2.0 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
@@ -73,15 +77,15 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
- github.com/spf13/pflag v1.0.5 // indirect
+ github.com/spf13/pflag v1.0.5
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
- golang.org/x/crypto v0.36.0
+ golang.org/x/crypto v0.43.0
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
- golang.org/x/net v0.38.0 // indirect
- golang.org/x/term v0.30.0
- golang.org/x/text v0.23.0
+ golang.org/x/net v0.46.0 // indirect
+ golang.org/x/term v0.36.0
+ golang.org/x/text v0.30.0
google.golang.org/protobuf v1.34.1
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 464cd6fcc89..83014fc5b05 100644
--- a/go.sum
+++ b/go.sum
@@ -14,7 +14,11 @@ github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6IC
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
@@ -123,6 +127,7 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
@@ -143,6 +148,8 @@ github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728 h1:QwWKgMY28TAXaDl+
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728/go.mod h1:1fEHWurg7pvf5SG6XNE5Q8UZmOwex51Mkx3SLhrW5B4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
@@ -207,6 +214,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
+github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xtgo/set v1.0.0 h1:6BCNBRv3ORNDQ7fyoJXRv+tstJz3m1JVFQErfeZz2pY=
@@ -224,8 +233,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
-golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
+golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -255,6 +264,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
+golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -267,8 +278,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
-golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
+golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -278,8 +289,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -295,17 +306,17 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
-golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
-golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
+golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
+golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
-golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -319,8 +330,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
-golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/harmony/harmonyparser.go b/harmony/harmonyparser.go
index da9fe3e938c..4f405dc3539 100644
--- a/harmony/harmonyparser.go
+++ b/harmony/harmonyparser.go
@@ -388,9 +388,9 @@ func NewFunctionNameMap() *FunctionNameMap {
}
}
-// Init initializes the handler with tools and optional last message
+// Init initializes the handler with tools, optional last message, and think value
// Implements the Parser interface
-func (h *HarmonyMessageHandler) Init(tools []api.Tool, lastMessage *api.Message) []api.Tool {
+func (h *HarmonyMessageHandler) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
// Initialize the harmony parser
if h.HarmonyParser == nil {
h.HarmonyParser = &HarmonyParser{
diff --git a/integration/embed_test.go b/integration/embed_test.go
index e155498dbf4..e4506673940 100644
--- a/integration/embed_test.go
+++ b/integration/embed_test.go
@@ -4,7 +4,9 @@ package integration
import (
"context"
+ "errors"
"math"
+ "strings"
"testing"
"time"
@@ -204,8 +206,8 @@ func TestAllMiniLMEmbed(t *testing.T) {
t.Fatalf("expected %v, got %v (similarity: %f)", expected[0:5], res.Embeddings[0][0:5], sim)
}
- if res.PromptEvalCount != 6 {
- t.Fatalf("expected 6 prompt tokens, got %d", res.PromptEvalCount)
+ if res.PromptEvalCount != 8 {
+ t.Fatalf("expected 8 prompt tokens, got %d", res.PromptEvalCount)
}
}
@@ -251,8 +253,8 @@ func TestAllMiniLMBatchEmbed(t *testing.T) {
t.Fatalf("expected %v, got %v (similarity: %f)", expected[1][0:5], res.Embeddings[1][0:5], sim)
}
- if res.PromptEvalCount != 12 {
- t.Fatalf("expected 12 prompt tokens, got %d", res.PromptEvalCount)
+ if res.PromptEvalCount != 16 {
+ t.Fatalf("expected 16 prompt tokens, got %d", res.PromptEvalCount)
}
}
@@ -275,7 +277,7 @@ func TestAllMiniLMEmbedTruncate(t *testing.T) {
cases := []struct {
name string
request api.EmbedRequest
- check func(*api.EmbedResponse, error)
+ check func(*testing.T, *api.EmbedResponse, error)
}{
{
name: "target truncation",
@@ -283,7 +285,7 @@ func TestAllMiniLMEmbedTruncate(t *testing.T) {
Model: "all-minilm",
Input: "why",
},
- check: func(got *api.EmbedResponse, err error) {
+ check: func(t *testing.T, got *api.EmbedResponse, err error) {
if err != nil {
t.Fatal(err)
}
@@ -300,10 +302,11 @@ func TestAllMiniLMEmbedTruncate(t *testing.T) {
Input: "why is the sky blue?",
Options: map[string]any{"num_ctx": 3},
},
- check: func(got *api.EmbedResponse, err error) {
+ check: func(t *testing.T, got *api.EmbedResponse, err error) {
if err != nil {
t.Fatal(err)
}
+ t.Logf("PromptEvalCount: want=%d got=%d", want.PromptEvalCount, got.PromptEvalCount)
if diff := cmp.Diff(want.Embeddings[0], got.Embeddings[0]); diff != "" {
t.Errorf("embedding mismatch (-want +got):\n%s", diff)
}
@@ -317,10 +320,11 @@ func TestAllMiniLMEmbedTruncate(t *testing.T) {
Truncate: &truncTrue,
Options: map[string]any{"num_ctx": 3},
},
- check: func(got *api.EmbedResponse, err error) {
+ check: func(t *testing.T, got *api.EmbedResponse, err error) {
if err != nil {
t.Fatal(err)
}
+ t.Logf("PromptEvalCount: want=%d got=%d", want.PromptEvalCount, got.PromptEvalCount)
if diff := cmp.Diff(want.Embeddings[0], got.Embeddings[0]); diff != "" {
t.Errorf("embedding mismatch (-want +got):\n%s", diff)
}
@@ -334,21 +338,21 @@ func TestAllMiniLMEmbedTruncate(t *testing.T) {
Truncate: &truncFalse,
Options: map[string]any{"num_ctx": 3},
},
- check: func(res *api.EmbedResponse, err error) {
- if err.Error() != "input exceeds maximum context length" {
+ check: func(t *testing.T, res *api.EmbedResponse, err error) {
+ if err.Error() != "the input length exceeds the context length" {
t.Fatalf("expected truncation error, got: %v", err)
}
},
},
{
- name: "input after truncate error",
+ name: "input after truncate error with context length of 1",
request: api.EmbedRequest{
Model: "all-minilm",
Input: "why is the sky blue?",
Truncate: &truncTrue,
Options: map[string]any{"num_ctx": 1},
},
- check: func(res *api.EmbedResponse, err error) {
+ check: func(t *testing.T, res *api.EmbedResponse, err error) {
if err.Error() != "input after truncation exceeds maximum context length" {
t.Fatalf("expected truncation error, got: %v", err)
}
@@ -362,7 +366,7 @@ func TestAllMiniLMEmbedTruncate(t *testing.T) {
Truncate: &truncTrue,
Options: map[string]any{"num_ctx": 0},
},
- check: func(res *api.EmbedResponse, err error) {
+ check: func(t *testing.T, res *api.EmbedResponse, err error) {
if err.Error() != "input after truncation exceeds maximum context length" {
t.Fatalf("expected truncation error, got: %v", err)
}
@@ -375,7 +379,7 @@ func TestAllMiniLMEmbedTruncate(t *testing.T) {
Input: "why is the sky blue? Why is the sky blue? hi there my",
Options: map[string]any{"num_ctx": 16},
},
- check: func(res *api.EmbedResponse, err error) {
+ check: func(t *testing.T, res *api.EmbedResponse, err error) {
if err != nil {
t.Fatal(err)
}
@@ -385,7 +389,8 @@ func TestAllMiniLMEmbedTruncate(t *testing.T) {
for _, req := range cases {
t.Run(req.name, func(t *testing.T) {
- req.check(embedTestHelper(ctx, client, t, req.request))
+ resp, err := embedTestHelper(ctx, client, t, req.request)
+ req.check(t, resp, err)
})
}
}
@@ -409,3 +414,230 @@ func embedTestHelper(ctx context.Context, client *api.Client, t *testing.T, req
return client.Embed(ctx, &req)
}
+
+func TestEmbedTruncation(t *testing.T) {
+ // Use test deadline if set, otherwise default to 2 minutes
+ timeout := 2 * time.Minute
+ if deadline, ok := t.Deadline(); ok {
+ timeout = time.Until(deadline) - 10*time.Second // Reserve 10s buffer
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+ client, _, cleanup := InitServerConnection(ctx, t)
+ defer cleanup()
+
+ for _, model := range libraryEmbedModels {
+ model := model
+ t.Run(model, func(t *testing.T) {
+ // Check if we're running out of time (reserve 20s for current model)
+ if deadline, ok := t.Deadline(); ok && time.Until(deadline) < 20*time.Second {
+ t.Skip("skipping remaining tests to avoid timeout")
+ }
+
+ // Give each model its own budget to account for first-time pulls/loads
+ mctx, mcancel := context.WithTimeout(ctx, 3*time.Minute)
+ defer mcancel()
+
+ t.Run("truncation batch", func(t *testing.T) {
+ truncTrue := true
+ req := api.EmbedRequest{
+ Model: model,
+ Input: []string{"short", strings.Repeat("long ", 100), "medium text"},
+ Truncate: &truncTrue,
+ Options: map[string]any{"num_ctx": 30},
+ }
+
+ res, err := embedTestHelper(mctx, client, t, req)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(res.Embeddings) != 3 {
+ t.Fatalf("expected 3 embeddings, got %d", len(res.Embeddings))
+ }
+
+ if res.PromptEvalCount > 90 {
+ t.Fatalf("expected tokens <= 90 (3 × 30 max), got %d", res.PromptEvalCount)
+ }
+ })
+
+ t.Run("runner token count accuracy", func(t *testing.T) {
+ baseline := api.EmbedRequest{Model: model, Input: "test"}
+ baseRes, err := embedTestHelper(mctx, client, t, baseline)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ batch := api.EmbedRequest{
+ Model: model,
+ Input: []string{"test", "test", "test"},
+ }
+ batchRes, err := embedTestHelper(mctx, client, t, batch)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expectedCount := baseRes.PromptEvalCount * 3
+ if batchRes.PromptEvalCount < expectedCount-2 || batchRes.PromptEvalCount > expectedCount+2 {
+ t.Fatalf("expected ~%d tokens (3 × %d), got %d",
+ expectedCount, baseRes.PromptEvalCount, batchRes.PromptEvalCount)
+ }
+ })
+ })
+ }
+}
+
+// TestEmbedLargeInput tests that embedding models can handle large inputs that would exceed typical batch sizes.
+func TestEmbedLargeInput(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
+ defer cancel()
+ client, _, cleanup := InitServerConnection(ctx, t)
+ defer cleanup()
+
+ for _, model := range libraryEmbedModels {
+ model := model
+ t.Run(model, func(t *testing.T) {
+ mctx, mcancel := context.WithTimeout(ctx, 2*time.Minute)
+ defer mcancel()
+
+ // Test with progressively larger inputs
+ testCases := []struct {
+ name string
+ inputWords int
+ }{
+ {"medium_input_256_words", 256},
+ {"large_input_512_words", 512},
+ {"very_large_input_800_words", 800},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ words := make([]string, tc.inputWords)
+ for i := range words {
+ words[i] = "word"
+ }
+ input := strings.Join(words, " ")
+
+ req := api.EmbedRequest{
+ Model: model,
+ Input: input,
+ KeepAlive: &api.Duration{Duration: 30 * time.Second},
+ }
+
+ res, err := embedTestHelper(mctx, client, t, req)
+ if err != nil {
+ t.Fatalf("embedding failed for %d words: %v", tc.inputWords, err)
+ }
+
+ if len(res.Embeddings) != 1 {
+ t.Fatalf("expected 1 embedding, got %d", len(res.Embeddings))
+ }
+
+ if len(res.Embeddings[0]) == 0 {
+ t.Fatal("expected non-empty embedding")
+ }
+
+ t.Logf("Successfully embedded %d words (%d tokens)", tc.inputWords, res.PromptEvalCount)
+ })
+ }
+ })
+ }
+}
+
+// TestEmbedStatusCode tests that errors from the embedding endpoint
+// properly preserve their HTTP status codes when returned to the client.
+// This test specifically checks the error handling path in EmbedHandler
+// where api.StatusError errors should maintain their original status code.
+func TestEmbedStatusCode(t *testing.T) {
+ // Use test deadline if set, otherwise default to 2 minutes
+ timeout := 2 * time.Minute
+ if deadline, ok := t.Deadline(); ok {
+ timeout = time.Until(deadline) - 10*time.Second // Reserve 10s buffer
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+ client, _, cleanup := InitServerConnection(ctx, t)
+ defer cleanup()
+
+ for _, model := range libraryEmbedModels {
+ model := model
+ t.Run(model, func(t *testing.T) {
+ // Check if we're running out of time (reserve 20s for current model)
+ if deadline, ok := t.Deadline(); ok && time.Until(deadline) < 20*time.Second {
+ t.Skip("skipping remaining tests to avoid timeout")
+ }
+
+ mctx, mcancel := context.WithTimeout(ctx, 3*time.Minute)
+ defer mcancel()
+
+ // Pull the model if needed
+ if err := PullIfMissing(mctx, client, model); err != nil {
+ t.Fatal(err)
+ }
+
+ t.Run("truncation error status code", func(t *testing.T) {
+ truncFalse := false
+ longInput := strings.Repeat("word ", 100)
+
+ req := api.EmbedRequest{
+ Model: model,
+ Input: longInput,
+ Truncate: &truncFalse,
+ Options: map[string]any{"num_ctx": 10},
+ }
+
+ _, err := embedTestHelper(mctx, client, t, req)
+ if err == nil {
+ t.Fatal("expected error when truncate=false with long input")
+ }
+
+ // Check that it's a StatusError with the correct status code
+ var statusErr api.StatusError
+ if !errors.As(err, &statusErr) {
+ t.Fatalf("expected api.StatusError, got %T: %v", err, err)
+ }
+
+ // The error should be a 4xx client error (likely 400 Bad Request)
+ // not a 500 Internal Server Error
+ if statusErr.StatusCode < 400 || statusErr.StatusCode >= 500 {
+ t.Errorf("expected 4xx status code, got %d", statusErr.StatusCode)
+ }
+
+ // Verify the error message is meaningful
+ if !strings.Contains(err.Error(), "context length") {
+ t.Errorf("expected error message to mention context length, got: %v", err)
+ }
+ })
+
+ t.Run("batch truncation error status code", func(t *testing.T) {
+ truncFalse := false
+ req := api.EmbedRequest{
+ Model: model,
+ Input: []string{
+ "short input",
+ strings.Repeat("very long input ", 100),
+ "another short input",
+ },
+ Truncate: &truncFalse,
+ Options: map[string]any{"num_ctx": 10},
+ }
+
+ _, err := embedTestHelper(mctx, client, t, req)
+ if err == nil {
+ t.Fatal("expected error when one input exceeds context with truncate=false")
+ }
+
+ // Check that it's a StatusError with the correct status code
+ var statusErr api.StatusError
+ if !errors.As(err, &statusErr) {
+ t.Fatalf("expected api.StatusError, got %T: %v", err, err)
+ }
+
+ // The error should be a 4xx client error, not a 500 Internal Server Error
+ if statusErr.StatusCode < 400 || statusErr.StatusCode >= 500 {
+ t.Errorf("expected 4xx status code, got %d", statusErr.StatusCode)
+ }
+ })
+ })
+ }
+}
diff --git a/integration/llm_image_test.go b/integration/llm_image_test.go
index e1c16bafdbf..1a99ddd2238 100644
--- a/integration/llm_image_test.go
+++ b/integration/llm_image_test.go
@@ -33,6 +33,9 @@ func TestVisionModels(t *testing.T) {
// Qwen 3 VL mixture of experts
model: "qwen3-vl:30b",
},
+ {
+ model: "ministral-3",
+ },
}
for _, v := range testCases {
diff --git a/integration/tools_test.go b/integration/tools_test.go
index d6b8dfa54d5..e74f40413bd 100644
--- a/integration/tools_test.go
+++ b/integration/tools_test.go
@@ -11,6 +11,15 @@ import (
"github.com/ollama/ollama/api"
)
+// testPropsMap creates a ToolPropertiesMap from a map (convenience function for tests)
+func testPropsMap(m map[string]api.ToolProperty) *api.ToolPropertiesMap {
+ props := api.NewToolPropertiesMap()
+ for k, v := range m {
+ props.Set(k, v)
+ }
+ return props
+}
+
func TestAPIToolCalling(t *testing.T) {
initialTimeout := 60 * time.Second
streamTimeout := 60 * time.Second
@@ -30,6 +39,7 @@ func TestAPIToolCalling(t *testing.T) {
"mistral": 6,
"qwen2.5": 6,
"qwen2": 6,
+ "ministral-3": 20,
"mistral-nemo": 9,
"mistral-small": 16,
"mixtral:8x22b": 80,
@@ -56,12 +66,12 @@ func TestAPIToolCalling(t *testing.T) {
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"location"},
- Properties: map[string]api.ToolProperty{
+ Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "The city and state, e.g. San Francisco, CA",
},
- },
+ }),
},
},
},
diff --git a/integration/utils_test.go b/integration/utils_test.go
index 8a362408e55..2dd39ecb0b8 100644
--- a/integration/utils_test.go
+++ b/integration/utils_test.go
@@ -38,6 +38,7 @@ var (
// Note: add newer models at the top of the list to test them first
ollamaEngineChatModels = []string{
+ "ministral-3",
"qwen3-coder:30b",
"gpt-oss:20b",
"gemma3n:e2b",
@@ -167,6 +168,7 @@ var (
"medllama2",
"megadolphin",
"minicpm-v",
+ "ministral-3",
"mistral-large",
"mistral-nemo",
"mistral-openorca",
@@ -270,6 +272,7 @@ var (
"mistral",
"qwen2.5",
"qwen2",
+ "ministral-3",
"mistral-nemo",
"mistral-small",
"mixtral:8x22b",
diff --git a/internal/orderedmap/orderedmap.go b/internal/orderedmap/orderedmap.go
new file mode 100644
index 00000000000..5ee5a94037e
--- /dev/null
+++ b/internal/orderedmap/orderedmap.go
@@ -0,0 +1,94 @@
+// Package orderedmap provides a generic ordered map that maintains insertion order.
+// It wraps github.com/wk8/go-ordered-map/v2 to encapsulate the dependency.
+package orderedmap
+
+import (
+ "encoding/json"
+ "iter"
+
+ orderedmap "github.com/wk8/go-ordered-map/v2"
+)
+
+// Map is a generic ordered map that maintains insertion order.
+type Map[K comparable, V any] struct {
+ om *orderedmap.OrderedMap[K, V]
+}
+
+// New creates a new empty ordered map.
+func New[K comparable, V any]() *Map[K, V] {
+ return &Map[K, V]{
+ om: orderedmap.New[K, V](),
+ }
+}
+
+// Get retrieves a value by key.
+func (m *Map[K, V]) Get(key K) (V, bool) {
+ if m == nil || m.om == nil {
+ var zero V
+ return zero, false
+ }
+ return m.om.Get(key)
+}
+
+// Set sets a key-value pair. If the key already exists, its value is updated
+// but its position in the iteration order is preserved. If the key is new,
+// it is appended to the end.
+func (m *Map[K, V]) Set(key K, value V) {
+ if m == nil {
+ return
+ }
+ if m.om == nil {
+ m.om = orderedmap.New[K, V]()
+ }
+ m.om.Set(key, value)
+}
+
+// Len returns the number of entries.
+func (m *Map[K, V]) Len() int {
+ if m == nil || m.om == nil {
+ return 0
+ }
+ return m.om.Len()
+}
+
+// All returns an iterator over all key-value pairs in insertion order.
+func (m *Map[K, V]) All() iter.Seq2[K, V] {
+ return func(yield func(K, V) bool) {
+ if m == nil || m.om == nil {
+ return
+ }
+ for pair := m.om.Oldest(); pair != nil; pair = pair.Next() {
+ if !yield(pair.Key, pair.Value) {
+ return
+ }
+ }
+ }
+}
+
+// ToMap converts to a regular Go map.
+// Note: The resulting map does not preserve order.
+func (m *Map[K, V]) ToMap() map[K]V {
+ if m == nil || m.om == nil {
+ return nil
+ }
+ result := make(map[K]V, m.om.Len())
+ for pair := m.om.Oldest(); pair != nil; pair = pair.Next() {
+ result[pair.Key] = pair.Value
+ }
+ return result
+}
+
+// MarshalJSON implements json.Marshaler. The JSON output preserves key order.
+func (m *Map[K, V]) MarshalJSON() ([]byte, error) {
+ if m == nil || m.om == nil {
+ return []byte("null"), nil
+ }
+ return json.Marshal(m.om)
+}
+
+// UnmarshalJSON implements json.Unmarshaler. The insertion order matches the
+// order of keys in the JSON input.
+func (m *Map[K, V]) UnmarshalJSON(data []byte) error {
+ m.om = orderedmap.New[K, V]()
+ return json.Unmarshal(data, &m.om)
+}
diff --git a/internal/orderedmap/orderedmap_test.go b/internal/orderedmap/orderedmap_test.go
new file mode 100644
index 00000000000..9886d24b7a1
--- /dev/null
+++ b/internal/orderedmap/orderedmap_test.go
@@ -0,0 +1,348 @@
+package orderedmap
+
+import (
+ "encoding/json"
+ "slices"
+ "testing"
+)
+
+func TestMap_BasicOperations(t *testing.T) {
+ m := New[string, int]()
+
+ // Test empty map
+ if m.Len() != 0 {
+ t.Errorf("expected Len() = 0, got %d", m.Len())
+ }
+ v, ok := m.Get("a")
+ if ok {
+ t.Error("expected Get on empty map to return false")
+ }
+ if v != 0 {
+ t.Errorf("expected zero value, got %d", v)
+ }
+
+ // Test Set and Get
+ m.Set("a", 1)
+ m.Set("b", 2)
+ m.Set("c", 3)
+
+ if m.Len() != 3 {
+ t.Errorf("expected Len() = 3, got %d", m.Len())
+ }
+
+ v, ok = m.Get("a")
+ if !ok || v != 1 {
+ t.Errorf("expected Get(a) = (1, true), got (%d, %v)", v, ok)
+ }
+
+ v, ok = m.Get("b")
+ if !ok || v != 2 {
+ t.Errorf("expected Get(b) = (2, true), got (%d, %v)", v, ok)
+ }
+
+ v, ok = m.Get("c")
+ if !ok || v != 3 {
+ t.Errorf("expected Get(c) = (3, true), got (%d, %v)", v, ok)
+ }
+
+ // Test updating existing key preserves position
+ m.Set("a", 10)
+ v, ok = m.Get("a")
+ if !ok || v != 10 {
+ t.Errorf("expected Get(a) = (10, true), got (%d, %v)", v, ok)
+ }
+ if m.Len() != 3 {
+ t.Errorf("expected Len() = 3 after update, got %d", m.Len())
+ }
+}
+
+func TestMap_InsertionOrderPreserved(t *testing.T) {
+ m := New[string, int]()
+
+ // Insert in non-alphabetical order
+ m.Set("z", 1)
+ m.Set("a", 2)
+ m.Set("m", 3)
+ m.Set("b", 4)
+
+ // Verify iteration order matches insertion order
+ var keys []string
+ var values []int
+ for k, v := range m.All() {
+ keys = append(keys, k)
+ values = append(values, v)
+ }
+
+ expectedKeys := []string{"z", "a", "m", "b"}
+ expectedValues := []int{1, 2, 3, 4}
+
+ if !slices.Equal(keys, expectedKeys) {
+ t.Errorf("expected keys %v, got %v", expectedKeys, keys)
+ }
+ if !slices.Equal(values, expectedValues) {
+ t.Errorf("expected values %v, got %v", expectedValues, values)
+ }
+}
+
+func TestMap_UpdatePreservesPosition(t *testing.T) {
+ m := New[string, int]()
+
+ m.Set("first", 1)
+ m.Set("second", 2)
+ m.Set("third", 3)
+
+ // Update middle element
+ m.Set("second", 20)
+
+ var keys []string
+ for k := range m.All() {
+ keys = append(keys, k)
+ }
+
+ // Order should still be first, second, third
+ expected := []string{"first", "second", "third"}
+ if !slices.Equal(keys, expected) {
+ t.Errorf("expected keys %v, got %v", expected, keys)
+ }
+}
+
+func TestMap_MarshalJSON_PreservesOrder(t *testing.T) {
+ m := New[string, int]()
+
+ // Insert in non-alphabetical order
+ m.Set("z", 1)
+ m.Set("a", 2)
+ m.Set("m", 3)
+
+ data, err := json.Marshal(m)
+ if err != nil {
+ t.Fatalf("Marshal failed: %v", err)
+ }
+
+ // JSON should preserve insertion order, not alphabetical
+ expected := `{"z":1,"a":2,"m":3}`
+ if string(data) != expected {
+ t.Errorf("expected %s, got %s", expected, string(data))
+ }
+}
+
+func TestMap_UnmarshalJSON_PreservesOrder(t *testing.T) {
+ // JSON with non-alphabetical key order
+ jsonData := `{"z":1,"a":2,"m":3}`
+
+ m := New[string, int]()
+ if err := json.Unmarshal([]byte(jsonData), m); err != nil {
+ t.Fatalf("Unmarshal failed: %v", err)
+ }
+
+ // Verify iteration order matches JSON order
+ var keys []string
+ for k := range m.All() {
+ keys = append(keys, k)
+ }
+
+ expected := []string{"z", "a", "m"}
+ if !slices.Equal(keys, expected) {
+ t.Errorf("expected keys %v, got %v", expected, keys)
+ }
+}
+
+func TestMap_JSONRoundTrip(t *testing.T) {
+ // Test that unmarshal -> marshal produces identical JSON
+ original := `{"zebra":"z","apple":"a","mango":"m","banana":"b"}`
+
+ m := New[string, string]()
+ if err := json.Unmarshal([]byte(original), m); err != nil {
+ t.Fatalf("Unmarshal failed: %v", err)
+ }
+
+ data, err := json.Marshal(m)
+ if err != nil {
+ t.Fatalf("Marshal failed: %v", err)
+ }
+
+ if string(data) != original {
+ t.Errorf("round trip failed: expected %s, got %s", original, string(data))
+ }
+}
+
+func TestMap_ToMap(t *testing.T) {
+ m := New[string, int]()
+ m.Set("a", 1)
+ m.Set("b", 2)
+
+ regular := m.ToMap()
+
+ if len(regular) != 2 {
+ t.Errorf("expected len 2, got %d", len(regular))
+ }
+ if regular["a"] != 1 {
+ t.Errorf("expected regular[a] = 1, got %d", regular["a"])
+ }
+ if regular["b"] != 2 {
+ t.Errorf("expected regular[b] = 2, got %d", regular["b"])
+ }
+}
+
+func TestMap_NilSafety(t *testing.T) {
+ var m *Map[string, int]
+
+ // All operations should be safe on nil
+ if m.Len() != 0 {
+ t.Errorf("expected Len() = 0 on nil map, got %d", m.Len())
+ }
+
+ v, ok := m.Get("a")
+ if ok {
+ t.Error("expected Get on nil map to return false")
+ }
+ if v != 0 {
+ t.Errorf("expected zero value from nil map, got %d", v)
+ }
+
+ // Set on nil is a no-op
+ m.Set("a", 1)
+ if m.Len() != 0 {
+ t.Errorf("expected Len() = 0 after Set on nil, got %d", m.Len())
+ }
+
+ // All returns empty iterator
+ var keys []string
+ for k := range m.All() {
+ keys = append(keys, k)
+ }
+ if len(keys) != 0 {
+ t.Errorf("expected empty iteration on nil map, got %v", keys)
+ }
+
+ // ToMap returns nil
+ if m.ToMap() != nil {
+ t.Error("expected ToMap to return nil on nil map")
+ }
+
+ // MarshalJSON returns null
+ data, err := json.Marshal(m)
+ if err != nil {
+ t.Fatalf("Marshal failed: %v", err)
+ }
+ if string(data) != "null" {
+ t.Errorf("expected null, got %s", string(data))
+ }
+}
+
+func TestMap_EmptyMapMarshal(t *testing.T) {
+ m := New[string, int]()
+
+ data, err := json.Marshal(m)
+ if err != nil {
+ t.Fatalf("Marshal failed: %v", err)
+ }
+ if string(data) != "{}" {
+ t.Errorf("expected {}, got %s", string(data))
+ }
+}
+
+func TestMap_NestedValues(t *testing.T) {
+ m := New[string, any]()
+ m.Set("string", "hello")
+ m.Set("number", 42)
+ m.Set("bool", true)
+ m.Set("nested", map[string]int{"x": 1})
+
+ data, err := json.Marshal(m)
+ if err != nil {
+ t.Fatalf("Marshal failed: %v", err)
+ }
+
+ expected := `{"string":"hello","number":42,"bool":true,"nested":{"x":1}}`
+ if string(data) != expected {
+ t.Errorf("expected %s, got %s", expected, string(data))
+ }
+}
+
+func TestMap_AllIteratorEarlyExit(t *testing.T) {
+ m := New[string, int]()
+ m.Set("a", 1)
+ m.Set("b", 2)
+ m.Set("c", 3)
+ m.Set("d", 4)
+
+ // Collect only first 2
+ var keys []string
+ for k := range m.All() {
+ keys = append(keys, k)
+ if len(keys) == 2 {
+ break
+ }
+ }
+
+ expected := []string{"a", "b"}
+ if !slices.Equal(keys, expected) {
+ t.Errorf("expected %v, got %v", expected, keys)
+ }
+}
+
+func TestMap_IntegerKeys(t *testing.T) {
+ m := New[int, string]()
+ m.Set(3, "three")
+ m.Set(1, "one")
+ m.Set(2, "two")
+
+ var keys []int
+ for k := range m.All() {
+ keys = append(keys, k)
+ }
+
+ // Should preserve insertion order, not numerical order
+ expected := []int{3, 1, 2}
+ if !slices.Equal(keys, expected) {
+ t.Errorf("expected %v, got %v", expected, keys)
+ }
+}
+
+func TestMap_UnmarshalIntoExisting(t *testing.T) {
+ m := New[string, int]()
+ m.Set("existing", 999)
+
+ // Unmarshal should replace contents
+ if err := json.Unmarshal([]byte(`{"new":1}`), m); err != nil {
+ t.Fatalf("Unmarshal failed: %v", err)
+ }
+
+ _, ok := m.Get("existing")
+ if ok {
+ t.Error("existing key should be gone after unmarshal")
+ }
+
+ v, ok := m.Get("new")
+ if !ok || v != 1 {
+ t.Errorf("expected Get(new) = (1, true), got (%d, %v)", v, ok)
+ }
+}
+
+func TestMap_LargeOrderPreservation(t *testing.T) {
+ m := New[string, int]()
+
+ // Create many keys in specific order
+ keys := make([]string, 100)
+ for i := range 100 {
+ keys[i] = string(rune('a' + (99 - i))) // reverse order: 'd', 'c', 'b', 'a' (extended)
+ if i >= 26 {
+ keys[i] = string(rune('A'+i-26)) + string(rune('a'+i%26))
+ }
+ }
+
+ for i, k := range keys {
+ m.Set(k, i)
+ }
+
+ // Verify order preserved
+ var resultKeys []string
+ for k := range m.All() {
+ resultKeys = append(resultKeys, k)
+ }
+
+ if !slices.Equal(keys, resultKeys) {
+ t.Error("large map should preserve insertion order")
+ }
+}
diff --git a/kvcache/causal.go b/kvcache/causal.go
index d804f3bf048..e04f828e533 100644
--- a/kvcache/causal.go
+++ b/kvcache/causal.go
@@ -3,7 +3,6 @@ package kvcache
import (
"errors"
"fmt"
- "log/slog"
"math"
"slices"
@@ -40,18 +39,18 @@ type Causal struct {
// ** current forward pass **
- // the active layer for Get and Put
- curLayer int
-
- // starting location for data storage for this batch
- curLoc int
-
// size of the current batch
curBatchSize int
+ // locations for data storage for this batch
+ curLoc ml.Tensor
+
// mask of the cache as used by this batch
curMask ml.Tensor
+ // the active layer for Get and Put
+ curLayer int
+
// locations in the cache that are needed for this batch
curCellRange cellRange
@@ -141,10 +140,6 @@ func (c *Causal) Init(backend ml.Backend, dtype ml.DType, maxSequences, capacity
c.config.CachePadding = 1
}
- if c.config.MaskBatchPadding == 0 {
- c.config.MaskBatchPadding = 1
- }
-
if c.config.MaskDType == ml.DTypeOther {
c.config.MaskDType = ml.DTypeF32
}
@@ -206,45 +201,47 @@ func (c *Causal) StartForward(ctx ml.Context, batch input.Batch, reserve bool) e
c.curPositions = batch.Positions
c.opts.Except = nil
+ var locs []int32
if !reserve {
c.updateSlidingWindow()
var err error
- c.curLoc, err = c.findStartLoc()
- if errors.Is(err, ErrKvCacheFull) {
- c.defrag()
- c.curLoc, err = c.findStartLoc()
- }
+ locs, err = c.findLocs()
if err != nil {
return err
}
for i, pos := range batch.Positions {
seq := batch.Sequences[i]
+ loc := int(locs[i])
- c.cells[c.curLoc+i] = cacheCell{pos: pos, sequences: []int{seq}}
+ c.cells[loc] = cacheCell{pos: pos, sequences: []int{seq}}
seqRange, ok := c.cellRanges[seq]
if !ok {
seqRange = newRange()
}
- seqRange.min = min(seqRange.min, c.curLoc+i)
- c.curCellRange.min = min(c.curCellRange.min, c.curLoc+i)
+ seqRange.min = min(seqRange.min, loc)
+ c.curCellRange.min = min(c.curCellRange.min, loc)
- seqRange.max = max(seqRange.max, c.curLoc+i)
- c.curCellRange.max = max(c.curCellRange.max, c.curLoc+i)
+ seqRange.max = max(seqRange.max, loc)
+ c.curCellRange.max = max(c.curCellRange.max, loc)
c.cellRanges[seq] = seqRange
}
} else {
// If we are reserving memory, don't update any of the cache metadata but set the size
// to the worst case.
- c.curLoc = 0
+ locs = make([]int32, c.curBatchSize)
+ for i := range locs {
+ locs[i] = int32(i)
+ }
c.curCellRange.min = 0
c.curCellRange.max = len(c.cells) - 1
}
+ c.curLoc = ctx.Input().FromInts(locs, len(locs))
c.curMask = c.buildMask(ctx)
return nil
@@ -257,22 +254,20 @@ func newRange() cellRange {
}
}
-// Find the first contiguous block of at least curBatchSize
-func (c *Causal) findStartLoc() (int, error) {
- var start, count int
+// Returns a slice of locations where each token in the batch should be stored
+func (c *Causal) findLocs() ([]int32, error) {
+ loc := make([]int32, 0, c.curBatchSize)
+
for i := range c.cells {
if len(c.cells[i].sequences) == 0 {
- count++
- if count >= c.curBatchSize {
- return start, nil
+ loc = append(loc, int32(i))
+ if len(loc) >= c.curBatchSize {
+ return loc, nil
}
- } else {
- start = i + 1
- count = 0
}
}
- return 0, fmt.Errorf("%w (cache: %v batch: %v)", ErrKvCacheFull, len(c.cells), c.curBatchSize)
+ return nil, fmt.Errorf("%w (cache: %v batch: %v)", ErrKvCacheFull, len(c.cells), c.curBatchSize)
}
func (c *Causal) updateSlidingWindow() {
@@ -365,15 +360,12 @@ func roundUp(length, pad int) int {
// token in the history should apply. This is based on both the sequence and causality (the
// position of the history is not ahead of the token in the batch).
func (c *Causal) buildMask(ctx ml.Context) ml.Tensor {
- // Align and pad the two dimensions as required by the backend
- batchSize := roundUp(c.curBatchSize, c.config.MaskBatchPadding)
-
c.curCellRange.min = roundDown(c.curCellRange.min, c.config.CachePadding)
c.curCellRange.max = roundUp(c.curCellRange.max+1, c.config.CachePadding) - 1
length := c.curCellRange.max - c.curCellRange.min + 1
- mask := make([]float32, batchSize*length)
+ mask := make([]float32, c.curBatchSize*length)
for i := range c.curBatchSize {
enabled := !slices.Contains(c.opts.Except, i)
@@ -387,13 +379,7 @@ func (c *Causal) buildMask(ctx ml.Context) ml.Tensor {
}
}
- // Mask out any padding tokens we added. For padding that we added to the cache history, this
- // has already been masked out because the sequence doesn't match.
- for i := c.curBatchSize * length; i < len(mask); i++ {
- mask[i] = float32(math.Inf(-1))
- }
-
- maskTensor := ctx.Input().FromFloats(mask, length, batchSize)
+ maskTensor := ctx.Input().FromFloats(mask, length, c.curBatchSize)
if c.config.MaskDType != ml.DTypeF32 {
maskTensor = maskTensor.Cast(ctx, c.config.MaskDType)
@@ -402,145 +388,6 @@ func (c *Causal) buildMask(ctx ml.Context) ml.Tensor {
return maskTensor
}
-func (c *Causal) moveCells(ctx ml.Context, src, dst, length int) {
- for i, key := range c.keys {
- if key == nil {
- continue
- }
-
- kHeadDim := key.Dim(0)
- numKVHeads := key.Dim(1)
- rowSize := key.Stride(2)
-
- kSrcView := key.View(ctx, rowSize*src, kHeadDim*numKVHeads*length)
- kDstView := key.View(ctx, rowSize*dst, kHeadDim*numKVHeads*length)
-
- value := c.values[i]
- var vSrcView, vDstView ml.Tensor
- if c.config.PermutedV {
- vHeadDim := value.Dim(1)
- elemSize := value.Stride(0)
-
- vSrcView = value.View(ctx, elemSize*src, length, len(c.cells)*elemSize, vHeadDim*numKVHeads)
- vDstView = value.View(ctx, elemSize*dst, length, len(c.cells)*elemSize, vHeadDim*numKVHeads)
- } else {
- vHeadDim := value.Dim(0)
- rowSize := value.Stride(2)
-
- vSrcView = value.View(ctx, rowSize*src, vHeadDim*numKVHeads*length)
- vDstView = value.View(ctx, rowSize*dst, vHeadDim*numKVHeads*length)
- }
-
- ctx.Forward(
- kSrcView.Copy(ctx, kDstView),
- vSrcView.Copy(ctx, vDstView),
- )
- }
-}
-
-func (c *Causal) defrag() {
- slog.Debug("defragmenting kv cache")
-
- // Defrag strategy:
- // - Search for empty holes at the beginning of the cache,
- // filling them with active data starting at the end
- // - If there are contiguous elements that need to be moved,
- // combine them into a single operation by holding new moves
- // until we see that the next one is non-contiguous
- // - Fill up the context with the maximum number of operations it
- // can hold then compute that and continue with a new context
- //
- // We could try to optimize placement by grouping blocks from
- // the same sequences together but most likely the next forward
- // pass will disrupt this anyways, so the real world benefit
- // seems limited as this time.
-
- ctx := c.backend.NewContext()
-
- // For every move, 6 tensors are required per layer (2 views and a
- // copy for each of k and v). We also need to refer to the original
- // k and v cache tensors - once per layer, not per move.
- layers := 0
- for _, key := range c.keys {
- if key == nil {
- continue
- }
- layers++
- }
-
- maxMoves := (ctx.MaxGraphNodes() - 2*layers) / (6 * layers)
- moves := 0
-
- var pendingSrc, pendingDst, pendingLen int
- src := len(c.cells) - 1
-
- for dst := 0; dst < src; dst++ {
- if len(c.cells[dst].sequences) == 0 {
- for ; src > dst; src-- {
- if len(c.cells[src].sequences) != 0 {
- c.cells[dst] = c.cells[src]
- c.cells[src] = cacheCell{}
-
- if pendingLen > 0 {
- if src == pendingSrc-pendingLen && dst == pendingDst+pendingLen {
- pendingSrc = src
- pendingLen++
- break
- } else {
- c.moveCells(ctx, pendingSrc, pendingDst, pendingLen)
- moves++
- }
- }
-
- pendingSrc = src
- pendingDst = dst
- pendingLen = 1
-
- break
- }
- }
- }
-
- if moves >= maxMoves {
- ctx.Compute()
- ctx.Close()
- ctx = c.backend.NewContext()
-
- moves = 0
- }
- }
-
- if pendingLen > 0 {
- c.moveCells(ctx, pendingSrc, pendingDst, pendingLen)
- moves++
- }
-
- if moves > 0 {
- ctx.Compute()
- }
- ctx.Close()
-
- // Reset range metadata
- for seq := range c.cellRanges {
- seqRange := newRange()
-
- for i, cell := range c.cells {
- if slices.Contains(cell.sequences, seq) {
- if i < seqRange.min {
- seqRange.min = i
- }
- if i > seqRange.max {
- seqRange.max = i
- }
- }
- }
-
- c.cellRanges[seq] = seqRange
- }
-
- c.updateSlidingWindow()
-}
-
func (c *Causal) SetLayer(layer int) {
c.curLayer = layer
}
@@ -625,18 +472,25 @@ func (c *Causal) Put(ctx ml.Context, key, value ml.Tensor) {
}
}
- rowSize := c.keys[c.curLayer].Stride(2)
- ctx.Forward(key.Copy(ctx, c.keys[c.curLayer].View(ctx, rowSize*c.curLoc, kHeadDim*numKVHeads*batchSize)))
+ key = key.Reshape(ctx, kHeadDim*numKVHeads, batchSize)
+ keyCache := c.keys[c.curLayer]
+ keyCache = keyCache.Reshape(ctx, kHeadDim*numKVHeads, len(c.cells))
+ ctx.Forward(keyCache.SetRows(ctx, key, c.curLoc))
if c.config.PermutedV {
- elemSize := c.values[c.curLayer].Stride(0)
+ value = value.Reshape(ctx, vHeadDim*numKVHeads, 1, batchSize)
+ value = value.Permute(ctx, 2, 0, 1, 3)
+
+ valueCache := c.values[c.curLayer]
+ valueCache = valueCache.Reshape(ctx, 1, len(c.cells), vHeadDim*numKVHeads)
- value = value.Permute(ctx, 1, 2, 0, 3)
- ctx.Forward(value.Copy(ctx, c.values[c.curLayer].View(ctx, elemSize*c.curLoc, batchSize, len(c.cells)*elemSize, vHeadDim*numKVHeads)))
+ ctx.Forward(valueCache.SetRows(ctx, value, c.curLoc))
} else {
- rowSize := c.values[c.curLayer].Stride(2)
+ value = value.Reshape(ctx, vHeadDim*numKVHeads, batchSize)
+ valueCache := c.values[c.curLayer]
+ valueCache = valueCache.Reshape(ctx, vHeadDim*numKVHeads, len(c.cells))
- ctx.Forward(value.Copy(ctx, c.values[c.curLayer].View(ctx, rowSize*c.curLoc, vHeadDim*numKVHeads*batchSize)))
+ ctx.Forward(valueCache.SetRows(ctx, value, c.curLoc))
}
}
diff --git a/kvcache/causal_test.go b/kvcache/causal_test.go
index dd0c0442727..aeda93bc68e 100644
--- a/kvcache/causal_test.go
+++ b/kvcache/causal_test.go
@@ -1,6 +1,7 @@
package kvcache
import (
+ "fmt"
"math"
"slices"
"testing"
@@ -20,217 +21,184 @@ type testCase struct {
expectedMask []float32
}
-func TestStore(t *testing.T) {
- backend := &testBackend{}
- cache := NewCausalCache(nil)
- defer cache.Close()
-
- cache.Init(backend, ml.DTypeF16, 1, 16, 16)
-
- tests := []testCase{
- {
- name: "FirstBatch",
- in: []float32{111, 211, 121, 221, 131, 231, 112, 212, 122, 222, 132, 232, 113, 213, 123, 223, 133, 233, 114, 214, 124, 224, 134, 234},
- inShape: []int{2, 3, 4},
- seqs: []int{0, 0, 0, 0},
- pos: []int32{0, 1, 2, 3},
- expected: []float32{111, 211, 121, 221, 131, 231, 112, 212, 122, 222, 132, 232, 113, 213, 123, 223, 133, 233, 114, 214, 124, 224, 134, 234},
- expectedShape: []int{2, 3, 4},
- expectedMask: []float32{0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, float32(math.Inf(-1)), 0, 0, 0, 0},
- },
- {
- name: "SecondBatch",
- in: []float32{115, 215, 125, 225, 135, 235},
- inShape: []int{2, 3, 1},
- seqs: []int{0},
- pos: []int32{4},
- expected: []float32{111, 211, 121, 221, 131, 231, 112, 212, 122, 222, 132, 232, 113, 213, 123, 223, 133, 233, 114, 214, 124, 224, 134, 234, 115, 215, 125, 225, 135, 235},
- expectedShape: []int{2, 3, 5},
- expectedMask: []float32{0, 0, 0, 0, 0},
- },
+func runPermutedVariants(t *testing.T, fn func(t *testing.T, backend *testBackend)) {
+ t.Helper()
+ for _, permuted := range []bool{false, true} {
+ t.Run(fmt.Sprintf("PermutedV=%t", permuted), func(t *testing.T) {
+ fn(t, &testBackend{permutedV: permuted})
+ })
}
+}
+
+func TestStore(t *testing.T) {
+ runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
+ cache := NewCausalCache(nil)
+ defer cache.Close()
- testCache(t, backend, cache, tests)
+ cache.Init(backend, ml.DTypeF16, 1, 16, 16)
+
+ tests := []testCase{
+ {
+ name: "FirstBatch",
+ in: []float32{111, 211, 121, 221, 131, 231, 112, 212, 122, 222, 132, 232, 113, 213, 123, 223, 133, 233, 114, 214, 124, 224, 134, 234},
+ inShape: []int{2, 3, 4},
+ seqs: []int{0, 0, 0, 0},
+ pos: []int32{0, 1, 2, 3},
+ expected: []float32{111, 211, 121, 221, 131, 231, 112, 212, 122, 222, 132, 232, 113, 213, 123, 223, 133, 233, 114, 214, 124, 224, 134, 234},
+ expectedShape: []int{2, 3, 4},
+ expectedMask: []float32{0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, float32(math.Inf(-1)), 0, 0, 0, 0},
+ },
+ {
+ name: "SecondBatch",
+ in: []float32{115, 215, 125, 225, 135, 235},
+ inShape: []int{2, 3, 1},
+ seqs: []int{0},
+ pos: []int32{4},
+ expected: []float32{111, 211, 121, 221, 131, 231, 112, 212, 122, 222, 132, 232, 113, 213, 123, 223, 133, 233, 114, 214, 124, 224, 134, 234, 115, 215, 125, 225, 135, 235},
+ expectedShape: []int{2, 3, 5},
+ expectedMask: []float32{0, 0, 0, 0, 0},
+ },
+ }
+
+ testCache(t, backend, cache, tests)
+ })
}
func TestSWA(t *testing.T) {
- backend := &testBackend{}
- cache := NewSWACache(1, nil)
- defer cache.Close()
-
- cache.Init(backend, ml.DTypeF16, 1, 16, 16)
-
- x := float32(math.Inf(-1))
-
- tests := []testCase{
- {
- name: "FirstBatch",
- in: []float32{1, 2, 3, 4},
- inShape: []int{1, 1, 4},
- seqs: []int{0, 0, 0, 0},
- pos: []int32{0, 1, 2, 3},
- expected: []float32{1, 2, 3, 4},
- expectedShape: []int{1, 1, 4},
- expectedMask: []float32{
- 0, x, x, x,
- 0, 0, x, x,
- x, 0, 0, x,
- x, x, 0, 0,
+ runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
+ cache := NewSWACache(1, nil)
+ defer cache.Close()
+
+ cache.Init(backend, ml.DTypeF16, 1, 16, 16)
+
+ x := float32(math.Inf(-1))
+
+ tests := []testCase{
+ {
+ name: "FirstBatch",
+ in: []float32{1, 2, 3, 4},
+ inShape: []int{1, 1, 4},
+ seqs: []int{0, 0, 0, 0},
+ pos: []int32{0, 1, 2, 3},
+ expected: []float32{1, 2, 3, 4},
+ expectedShape: []int{1, 1, 4},
+ expectedMask: []float32{
+ 0, x, x, x,
+ 0, 0, x, x,
+ x, 0, 0, x,
+ x, x, 0, 0,
+ },
},
- },
- {
- name: "SecondBatch",
- in: []float32{5, 6},
- inShape: []int{1, 1, 2},
- seqs: []int{0, 0},
- pos: []int32{4, 5},
- expected: []float32{5, 6, 3, 4},
- expectedShape: []int{1, 1, 4},
- expectedMask: []float32{
- 0, x, x, 0,
- 0, 0, x, x,
+ {
+ name: "SecondBatch",
+ in: []float32{5, 6},
+ inShape: []int{1, 1, 2},
+ seqs: []int{0, 0},
+ pos: []int32{4, 5},
+ expected: []float32{5, 6, 3, 4},
+ expectedShape: []int{1, 1, 4},
+ expectedMask: []float32{
+ 0, x, x, 0,
+ 0, 0, x, x,
+ },
},
- },
- }
+ }
- testCache(t, backend, cache, tests)
+ testCache(t, backend, cache, tests)
+ })
}
func TestSWASeparateBatches(t *testing.T) {
- backend := &testBackend{}
- cache := NewSWACache(1, nil)
- defer cache.Close()
-
- cache.Init(backend, ml.DTypeF16, 2, 16, 2)
-
- x := float32(math.Inf(-1))
-
- tests := []testCase{
- {
- name: "First seq 0",
- in: []float32{1, 2},
- inShape: []int{1, 1, 2},
- seqs: []int{0, 0},
- pos: []int32{0, 1},
- expected: []float32{1, 2},
- expectedShape: []int{1, 1, 2},
- expectedMask: []float32{
- 0, x,
- 0, 0,
+ runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
+ cache := NewSWACache(1, nil)
+ defer cache.Close()
+
+ cache.Init(backend, ml.DTypeF16, 2, 16, 2)
+
+ x := float32(math.Inf(-1))
+
+ tests := []testCase{
+ {
+ name: "First seq 0",
+ in: []float32{1, 2},
+ inShape: []int{1, 1, 2},
+ seqs: []int{0, 0},
+ pos: []int32{0, 1},
+ expected: []float32{1, 2},
+ expectedShape: []int{1, 1, 2},
+ expectedMask: []float32{
+ 0, x,
+ 0, 0,
+ },
},
- },
- {
- name: "Second seq 0",
- in: []float32{3, 4},
- inShape: []int{1, 1, 2},
- seqs: []int{0, 0},
- pos: []int32{2, 3},
- expected: []float32{2, 3, 4},
- expectedShape: []int{1, 1, 3},
- expectedMask: []float32{
- 0, 0, x,
- x, 0, 0,
+ {
+ name: "Second seq 0",
+ in: []float32{3, 4},
+ inShape: []int{1, 1, 2},
+ seqs: []int{0, 0},
+ pos: []int32{2, 3},
+ expected: []float32{2, 3, 4},
+ expectedShape: []int{1, 1, 3},
+ expectedMask: []float32{
+ 0, 0, x,
+ x, 0, 0,
+ },
},
- },
- {
- name: "First seq 1",
- in: []float32{5, 6},
- inShape: []int{1, 1, 2},
- seqs: []int{1, 1},
- pos: []int32{0, 1},
- expected: []float32{5, 6},
- expectedShape: []int{1, 1, 2},
- expectedMask: []float32{
- 0, x,
- 0, 0,
+ {
+ name: "First seq 1",
+ in: []float32{5, 6},
+ inShape: []int{1, 1, 2},
+ seqs: []int{1, 1},
+ pos: []int32{0, 1},
+ expected: []float32{5, 6},
+ expectedShape: []int{1, 1, 2},
+ expectedMask: []float32{
+ 0, x,
+ 0, 0,
+ },
},
- },
- {
- name: "Second seq 1",
- in: []float32{7, 8},
- inShape: []int{1, 1, 2},
- seqs: []int{1, 1},
- pos: []int32{2, 3},
- expected: []float32{6, 3, 4, 7, 8},
- expectedShape: []int{1, 1, 5},
- expectedMask: []float32{
- 0, x, x, 0, x,
- x, x, x, 0, 0,
+ {
+ name: "Second seq 1",
+ in: []float32{7, 8},
+ inShape: []int{1, 1, 2},
+ seqs: []int{1, 1},
+ pos: []int32{2, 3},
+ expected: []float32{6, 3, 4, 7, 8},
+ expectedShape: []int{1, 1, 5},
+ expectedMask: []float32{
+ 0, x, x, 0, x,
+ x, x, x, 0, 0,
+ },
},
- },
- {
- name: "Third seq 0",
- in: []float32{9, 10},
- inShape: []int{1, 1, 2},
- seqs: []int{0, 0},
- pos: []int32{4, 5},
- expected: []float32{9, 10, 3, 4},
- expectedShape: []int{1, 1, 4},
- expectedMask: []float32{
- 0, x, x, 0,
- 0, 0, x, x,
+ {
+ name: "Third seq 0",
+ in: []float32{9, 10},
+ inShape: []int{1, 1, 2},
+ seqs: []int{0, 0},
+ pos: []int32{4, 5},
+ expected: []float32{9, 10, 3, 4},
+ expectedShape: []int{1, 1, 4},
+ expectedMask: []float32{
+ 0, x, x, 0,
+ 0, 0, x, x,
+ },
},
- },
- }
+ }
- testCache(t, backend, cache, tests)
+ testCache(t, backend, cache, tests)
+ })
}
func TestSWAMem(t *testing.T) {
- backend := &testBackend{}
- cache := NewSWAMemCache(1, 3, nil)
- defer cache.Close()
-
- cache.Init(backend, ml.DTypeF16, 1, 16, 16)
-
- x := float32(math.Inf(-1))
-
- tests := []testCase{
- {
- name: "FirstBatch",
- in: []float32{1, 2, 3, 4},
- inShape: []int{1, 1, 4},
- seqs: []int{0, 0, 0, 0},
- pos: []int32{0, 1, 2, 3},
- expected: []float32{1, 2, 3, 4},
- expectedShape: []int{1, 1, 4},
- expectedMask: []float32{
- 0, x, x, x,
- 0, 0, x, x,
- x, 0, 0, x,
- x, x, 0, 0,
- },
- },
- {
- name: "SecondBatch",
- in: []float32{5, 6},
- inShape: []int{1, 1, 2},
- seqs: []int{0, 0},
- pos: []int32{4, 5},
- expected: []float32{4, 5, 6},
- expectedShape: []int{1, 1, 3},
- expectedMask: []float32{
- 0, 0, x,
- x, 0, 0,
- },
- },
- }
-
- testCache(t, backend, cache, tests)
-}
-
-func TestChunkedAttention(t *testing.T) {
- cache := NewChunkedAttentionCache(2, nil)
- defer cache.Close()
+ runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
+ cache := NewSWAMemCache(1, 3, nil)
+ defer cache.Close()
- var b testBackend
- cache.Init(&b, ml.DTypeF16, 1, 16, 16)
+ cache.Init(backend, ml.DTypeF16, 1, 16, 16)
- x := float32(math.Inf(-1))
+ x := float32(math.Inf(-1))
- testCache(
- t, &b, cache,
- []testCase{
+ tests := []testCase{
{
name: "FirstBatch",
in: []float32{1, 2, 3, 4},
@@ -242,227 +210,240 @@ func TestChunkedAttention(t *testing.T) {
expectedMask: []float32{
0, x, x, x,
0, 0, x, x,
- x, x, 0, x,
+ x, 0, 0, x,
x, x, 0, 0,
},
},
{
name: "SecondBatch",
- in: []float32{5, 6, 7},
- inShape: []int{1, 1, 3},
- seqs: []int{0, 0, 0},
- pos: []int32{4, 5, 6},
- expected: []float32{1, 2, 3, 4, 5, 6, 7},
- expectedShape: []int{1, 1, 7},
- expectedMask: []float32{
- x, x, x, x, 0, x, x,
- x, x, x, x, 0, 0, x,
- x, x, x, x, x, x, 0,
- },
- },
- {
- name: "ThirdBatch",
- in: []float32{8, 9},
+ in: []float32{5, 6},
inShape: []int{1, 1, 2},
seqs: []int{0, 0},
- pos: []int32{7, 8},
- expected: []float32{1, 2, 3, 4, 5, 6, 7, 8, 9},
- expectedShape: []int{1, 1, 9},
+ pos: []int32{4, 5},
+ expected: []float32{5, 2, 3, 4, 6},
+ expectedShape: []int{1, 1, 5},
expectedMask: []float32{
- x, x, x, x, x, x, 0, 0, x,
- x, x, x, x, x, x, x, x, 0,
+ 0, x, x, 0, x,
+ 0, x, x, x, 0,
},
},
- },
- )
+ }
+
+ testCache(t, backend, cache, tests)
+ })
+}
+
+func TestChunkedAttention(t *testing.T) {
+ runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
+ cache := NewChunkedAttentionCache(2, nil)
+ defer cache.Close()
+
+ cache.Init(backend, ml.DTypeF16, 1, 16, 16)
+
+ x := float32(math.Inf(-1))
+
+ testCache(
+ t, backend, cache,
+ []testCase{
+ {
+ name: "FirstBatch",
+ in: []float32{1, 2, 3, 4},
+ inShape: []int{1, 1, 4},
+ seqs: []int{0, 0, 0, 0},
+ pos: []int32{0, 1, 2, 3},
+ expected: []float32{1, 2, 3, 4},
+ expectedShape: []int{1, 1, 4},
+ expectedMask: []float32{
+ 0, x, x, x,
+ 0, 0, x, x,
+ x, x, 0, x,
+ x, x, 0, 0,
+ },
+ },
+ {
+ name: "SecondBatch",
+ in: []float32{5, 6, 7},
+ inShape: []int{1, 1, 3},
+ seqs: []int{0, 0, 0},
+ pos: []int32{4, 5, 6},
+ expected: []float32{1, 2, 3, 4, 5, 6, 7},
+ expectedShape: []int{1, 1, 7},
+ expectedMask: []float32{
+ x, x, x, x, 0, x, x,
+ x, x, x, x, 0, 0, x,
+ x, x, x, x, x, x, 0,
+ },
+ },
+ {
+ name: "ThirdBatch",
+ in: []float32{8, 9},
+ inShape: []int{1, 1, 2},
+ seqs: []int{0, 0},
+ pos: []int32{7, 8},
+ expected: []float32{1, 2, 3, 4, 5, 6, 7, 8, 9},
+ expectedShape: []int{1, 1, 9},
+ expectedMask: []float32{
+ x, x, x, x, x, x, 0, 0, x,
+ x, x, x, x, x, x, x, x, 0,
+ },
+ },
+ },
+ )
+ })
}
func TestSequences(t *testing.T) {
- backend := &testBackend{}
- cache := NewCausalCache(nil)
- defer cache.Close()
-
- cache.Init(backend, ml.DTypeF16, 1, 16, 16)
-
- tests := []testCase{
- {
- name: "FirstBatch",
- in: []float32{1, 2, 3, 4},
- inShape: []int{1, 1, 4},
- seqs: []int{0, 0, 1, 1},
- pos: []int32{0, 1, 0, 1},
- expected: []float32{1, 2, 3, 4},
- expectedShape: []int{1, 1, 4},
- expectedMask: []float32{0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0},
- },
- {
- name: "SecondBatch",
- in: []float32{5, 6},
- inShape: []int{1, 1, 2},
- seqs: []int{0, 1},
- pos: []int32{2, 2},
- expected: []float32{1, 2, 3, 4, 5, 6},
- expectedShape: []int{1, 1, 6},
- expectedMask: []float32{0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), 0},
- },
- }
+ runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
+ cache := NewCausalCache(nil)
+ defer cache.Close()
+
+ cache.Init(backend, ml.DTypeF16, 1, 16, 16)
- testCache(t, backend, cache, tests)
+ tests := []testCase{
+ {
+ name: "FirstBatch",
+ in: []float32{1, 2, 3, 4},
+ inShape: []int{1, 1, 4},
+ seqs: []int{0, 0, 1, 1},
+ pos: []int32{0, 1, 0, 1},
+ expected: []float32{1, 2, 3, 4},
+ expectedShape: []int{1, 1, 4},
+ expectedMask: []float32{0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0},
+ },
+ {
+ name: "SecondBatch",
+ in: []float32{5, 6},
+ inShape: []int{1, 1, 2},
+ seqs: []int{0, 1},
+ pos: []int32{2, 2},
+ expected: []float32{1, 2, 3, 4, 5, 6},
+ expectedShape: []int{1, 1, 6},
+ expectedMask: []float32{0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), 0},
+ },
+ }
+
+ testCache(t, backend, cache, tests)
+ })
}
func TestRemove(t *testing.T) {
- backend := &testBackend{}
- cache := NewCausalCache(func(ctx ml.Context, layer int, key, shift ml.Tensor) (ml.Tensor, error) {
- return key.Add(ctx, shift), nil
- })
- defer cache.Close()
-
- cache.Init(backend, ml.DTypeF16, 1, 16, 16)
-
- tests := []testCase{
- {
- name: "FirstBatch",
- in: []float32{1, 2, 3, 4},
- inShape: []int{1, 1, 4},
- seqs: []int{0, 0, 1, 1},
- pos: []int32{0, 1, 0, 1},
- expected: []float32{1, 2, 3, 4},
- expectedShape: []int{1, 1, 4},
- expectedMask: []float32{0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0},
- },
- }
+ runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
+ cache := NewCausalCache(func(ctx ml.Context, layer int, key, shift ml.Tensor) (ml.Tensor, error) {
+ return key.Add(ctx, shift), nil
+ })
+ defer cache.Close()
- testCache(t, backend, cache, tests)
+ cache.Init(backend, ml.DTypeF16, 1, 16, 16)
- err := cache.Remove(0, 1, math.MaxInt32)
- if err != nil {
- panic(err)
- }
+ x := float32(math.Inf(-1))
- tests = []testCase{
- {
- name: "RemoveEnd",
- in: []float32{5, 6},
- inShape: []int{1, 1, 2},
- seqs: []int{0, 1},
- pos: []int32{1, 2},
- expected: []float32{1, 2, 3, 4, 5, 6},
- expectedShape: []int{1, 1, 6},
- expectedMask: []float32{0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), 0},
- },
- }
+ tests := []testCase{
+ {
+ name: "FirstBatch",
+ in: []float32{1, 2, 3, 4},
+ inShape: []int{1, 1, 4},
+ seqs: []int{0, 0, 1, 1},
+ pos: []int32{0, 1, 0, 1},
+ expected: []float32{1, 2, 3, 4},
+ expectedShape: []int{1, 1, 4},
+ expectedMask: []float32{
+ 0, x, x, x,
+ 0, 0, x, x,
+ x, x, 0, x,
+ x, x, 0, 0,
+ },
+ },
+ }
- testCache(t, backend, cache, tests)
+ testCache(t, backend, cache, tests)
- err = cache.Remove(0, 0, 1)
- if err != nil {
- panic(err)
- }
+ err := cache.Remove(0, 1, math.MaxInt32)
+ if err != nil {
+ panic(err)
+ }
- tests = []testCase{
- {
- name: "RemoveMiddle",
- in: []float32{7, 8},
- inShape: []int{1, 1, 2},
- seqs: []int{0, 0},
- pos: []int32{1, 2},
- expected: []float32{7, 8, 3, 4, 4},
- expectedShape: []int{1, 1, 5},
- expectedMask: []float32{0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0},
- },
- }
+ tests = []testCase{
+ {
+ name: "RemoveEnd",
+ in: []float32{5, 6},
+ inShape: []int{1, 1, 2},
+ seqs: []int{0, 1},
+ pos: []int32{1, 2},
+ expected: []float32{1, 5, 3, 4, 6},
+ expectedShape: []int{1, 1, 5},
+ expectedMask: []float32{
+ 0, 0, x, x, x,
+ x, x, 0, 0, 0,
+ },
+ },
+ }
- testCache(t, backend, cache, tests)
-}
+ testCache(t, backend, cache, tests)
-func TestDefrag(t *testing.T) {
- backend := &testBackend{}
- cache := NewCausalCache(func(ctx ml.Context, layer int, key, shift ml.Tensor) (ml.Tensor, error) {
- return key.Add(ctx, shift), nil
- })
- defer cache.Close()
-
- cache.Init(backend, ml.DTypeF16, 1, 16, 16)
-
- tests := []testCase{
- {
- name: "FirstBatch",
- in: []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
- inShape: []int{1, 1, 16},
- seqs: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- pos: []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
- expected: []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
- expectedShape: []int{1, 1, 16},
- expectedMask: []float32{0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- },
- }
+ err = cache.Remove(0, 0, 1)
+ if err != nil {
+ panic(err)
+ }
+
+ tests = []testCase{
+ {
+ name: "RemoveMiddle",
+ in: []float32{7, 8},
+ inShape: []int{1, 1, 2},
+ seqs: []int{0, 0},
+ pos: []int32{1, 2},
+ expected: []float32{7, 4, 3, 4, 6, 8},
+ expectedShape: []int{1, 1, 6},
+ expectedMask: []float32{
+ 0, 0, x, x, x, x,
+ 0, 0, x, x, x, 0,
+ },
+ },
+ }
- testCache(t, backend, cache, tests)
+ testCache(t, backend, cache, tests)
+ })
+}
- err := cache.Remove(0, 2, 4)
- if err != nil {
- panic(err)
- }
+func TestCopy(t *testing.T) {
+ runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
+ cache := NewCausalCache(func(ctx ml.Context, layer int, key, shift ml.Tensor) (ml.Tensor, error) { return key, nil })
+ defer cache.Close()
- err = cache.Remove(0, 13, math.MaxInt32)
- if err != nil {
- panic(err)
- }
+ cache.Init(backend, ml.DTypeF16, 1, 16, 16)
- tests = []testCase{
- {
- name: "Defrag",
- in: []float32{17, 18, 19},
- inShape: []int{1, 1, 3},
- seqs: []int{0, 0, 0},
- pos: []int32{16, 17, 18},
- expected: []float32{1, 2, 12, 13, 3, 4, 5, 6, 7, 8, 9, 10, 11, 17, 18, 19},
- expectedShape: []int{1, 1, 16},
- expectedMask: []float32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, float32(math.Inf(-1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- },
- }
+ tests := []testCase{
+ {
+ name: "FirstBatch",
+ in: []float32{1, 2, 3, 4},
+ inShape: []int{1, 1, 4},
+ seqs: []int{0, 0, 0, 0},
+ pos: []int32{0, 1, 2, 3},
+ expected: []float32{1, 2, 3, 4},
+ expectedShape: []int{1, 1, 4},
+ expectedMask: []float32{0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, float32(math.Inf(-1)), 0, 0, 0, 0},
+ },
+ }
- testCache(t, backend, cache, tests)
-}
+ testCache(t, backend, cache, tests)
-func TestCopy(t *testing.T) {
- backend := &testBackend{}
- cache := NewCausalCache(func(ctx ml.Context, layer int, key, shift ml.Tensor) (ml.Tensor, error) { return key, nil })
- defer cache.Close()
-
- cache.Init(backend, ml.DTypeF16, 1, 16, 16)
-
- tests := []testCase{
- {
- name: "FirstBatch",
- in: []float32{1, 2, 3, 4},
- inShape: []int{1, 1, 4},
- seqs: []int{0, 0, 0, 0},
- pos: []int32{0, 1, 2, 3},
- expected: []float32{1, 2, 3, 4},
- expectedShape: []int{1, 1, 4},
- expectedMask: []float32{0, float32(math.Inf(-1)), float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0, 0, float32(math.Inf(-1)), 0, 0, 0, 0},
- },
- }
+ cache.CopyPrefix(0, 1, 2)
- testCache(t, backend, cache, tests)
-
- cache.CopyPrefix(0, 1, 2)
-
- tests = []testCase{
- {
- name: "Copy",
- in: []float32{5, 6},
- inShape: []int{1, 1, 2},
- seqs: []int{1, 1},
- pos: []int32{3, 4},
- expected: []float32{1, 2, 3, 4, 5, 6},
- expectedShape: []int{1, 1, 6},
- expectedMask: []float32{0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0},
- },
- }
+ tests = []testCase{
+ {
+ name: "Copy",
+ in: []float32{5, 6},
+ inShape: []int{1, 1, 2},
+ seqs: []int{1, 1},
+ pos: []int32{3, 4},
+ expected: []float32{1, 2, 3, 4, 5, 6},
+ expectedShape: []int{1, 1, 6},
+ expectedMask: []float32{0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, float32(math.Inf(-1)), 0, 0, float32(math.Inf(-1)), float32(math.Inf(-1)), 0, 0},
+ },
+ }
- testCache(t, backend, cache, tests)
+ testCache(t, backend, cache, tests)
+ })
}
func testCache(t *testing.T, backend ml.Backend, cache Cache, tests []testCase) {
@@ -500,145 +481,148 @@ func testCache(t *testing.T, backend ml.Backend, cache Cache, tests []testCase)
}
func TestCanResume(t *testing.T) {
- backend := &testBackend{}
- windowSize := int32(4)
- cache := NewSWACache(windowSize, nil)
- defer cache.Close()
-
- cache.Init(backend, ml.DTypeF16, 1, 16, 16)
-
- context := backend.NewContext()
- defer context.Close()
-
- err := cache.StartForward(context, input.Batch{
- Positions: []int32{0, 1, 2, 3, 4},
- Sequences: []int{0, 0, 0, 0, 0},
- }, false)
- if err != nil {
- t.Fatalf("StartForward failed: %v", err)
- }
+ runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
+ windowSize := int32(4)
+ cache := NewSWACache(windowSize, nil)
+ defer cache.Close()
+
+ cache.Init(backend, ml.DTypeF16, 1, 16, 16)
+
+ context := backend.NewContext()
+ defer context.Close()
+
+ err := cache.StartForward(context, input.Batch{
+ Positions: []int32{0, 1, 2, 3, 4},
+ Sequences: []int{0, 0, 0, 0, 0},
+ }, false)
+ if err != nil {
+ t.Fatalf("StartForward failed: %v", err)
+ }
- cache.SetLayer(0)
- tensor := context.FromFloats([]float32{1, 2, 3, 4, 5}, 1, 1, 5)
- cache.Put(context, tensor, tensor)
+ cache.SetLayer(0)
+ tensor := context.FromFloats([]float32{1, 2, 3, 4, 5}, 1, 1, 5)
+ cache.Put(context, tensor, tensor)
- // with window size 4, nothing has slid out of the window yet
- if !cache.CanResume(0, 0) {
- t.Errorf("CanResume(0, 0) = false, want true (within window)")
- }
- if !cache.CanResume(0, 1) {
- t.Errorf("CanResume(0, 1) = false, want true (within window)")
- }
- if !cache.CanResume(0, 2) {
- t.Errorf("CanResume(0, 2) = false, want true (within window)")
- }
- if !cache.CanResume(0, 3) {
- t.Errorf("CanResume(0, 3) = false, want true (latest position)")
- }
- if !cache.CanResume(0, 4) {
- t.Errorf("CanResume(0, 4) = false, want true (latest position)")
- }
+ // with window size 4, nothing has slid out of the window yet
+ if !cache.CanResume(0, 0) {
+ t.Errorf("CanResume(0, 0) = false, want true (within window)")
+ }
+ if !cache.CanResume(0, 1) {
+ t.Errorf("CanResume(0, 1) = false, want true (within window)")
+ }
+ if !cache.CanResume(0, 2) {
+ t.Errorf("CanResume(0, 2) = false, want true (within window)")
+ }
+ if !cache.CanResume(0, 3) {
+ t.Errorf("CanResume(0, 3) = false, want true (latest position)")
+ }
+ if !cache.CanResume(0, 4) {
+ t.Errorf("CanResume(0, 4) = false, want true (latest position)")
+ }
- // shift window by adding position 5
- err = cache.StartForward(context, input.Batch{
- Positions: []int32{5},
- Sequences: []int{0},
- }, false)
- if err != nil {
- t.Fatalf("StartForward failed: %v", err)
- }
+ // shift window by adding position 5
+ err = cache.StartForward(context, input.Batch{
+ Positions: []int32{5},
+ Sequences: []int{0},
+ }, false)
+ if err != nil {
+ t.Fatalf("StartForward failed: %v", err)
+ }
- cache.SetLayer(0)
- tensor = context.FromFloats([]float32{6}, 1, 1, 1)
- cache.Put(context, tensor, tensor)
+ cache.SetLayer(0)
+ tensor = context.FromFloats([]float32{6}, 1, 1, 1)
+ cache.Put(context, tensor, tensor)
- // only the latest position has overlapping windows
- if cache.CanResume(0, 0) {
- t.Errorf("after shift: CanResume(0, 0) = true, want false (outside window)")
- }
- if cache.CanResume(0, 1) {
- t.Errorf("after shift: CanResume(0, 1) = true, want false (outside window)")
- }
- if cache.CanResume(0, 2) {
- t.Errorf("after shift: CanResume(0, 2) = true, want false (outside window)")
- }
- if cache.CanResume(0, 3) {
- t.Errorf("after shift: CanResume(0, 3) = true, want false (outside window)")
- }
- if cache.CanResume(0, 4) {
- t.Errorf("after shift: CanResume(0, 4) = true, want false (outside window)")
- }
- if !cache.CanResume(0, 5) {
- t.Errorf("after shift: CanResume(0, 5) = false, want true (latest position)")
- }
+ // only the latest position has overlapping windows
+ if cache.CanResume(0, 0) {
+ t.Errorf("after shift: CanResume(0, 0) = true, want false (outside window)")
+ }
+ if cache.CanResume(0, 1) {
+ t.Errorf("after shift: CanResume(0, 1) = true, want false (outside window)")
+ }
+ if cache.CanResume(0, 2) {
+ t.Errorf("after shift: CanResume(0, 2) = true, want false (outside window)")
+ }
+ if cache.CanResume(0, 3) {
+ t.Errorf("after shift: CanResume(0, 3) = true, want false (outside window)")
+ }
+ if cache.CanResume(0, 4) {
+ t.Errorf("after shift: CanResume(0, 4) = true, want false (outside window)")
+ }
+ if !cache.CanResume(0, 5) {
+ t.Errorf("after shift: CanResume(0, 5) = false, want true (latest position)")
+ }
+ })
}
func TestCanResumeSWAMem(t *testing.T) {
- backend := &testBackend{}
- windowSize := int32(4)
- memSize := int32(5)
- cache := NewSWAMemCache(windowSize, memSize, nil)
- defer cache.Close()
-
- cache.Init(backend, ml.DTypeF16, 1, 16, 16)
-
- context := backend.NewContext()
- defer context.Close()
-
- err := cache.StartForward(context, input.Batch{
- Positions: []int32{0, 1, 2, 3, 4, 5, 6},
- Sequences: []int{0, 0, 0, 0, 0, 0, 0},
- }, false)
- if err != nil {
- t.Fatalf("StartForward failed: %v", err)
- }
+ runPermutedVariants(t, func(t *testing.T, backend *testBackend) {
+ windowSize := int32(4)
+ memSize := int32(5)
+ cache := NewSWAMemCache(windowSize, memSize, nil)
+ defer cache.Close()
+
+ cache.Init(backend, ml.DTypeF16, 1, 16, 16)
+
+ context := backend.NewContext()
+ defer context.Close()
+
+ err := cache.StartForward(context, input.Batch{
+ Positions: []int32{0, 1, 2, 3, 4, 5, 6},
+ Sequences: []int{0, 0, 0, 0, 0, 0, 0},
+ }, false)
+ if err != nil {
+ t.Fatalf("StartForward failed: %v", err)
+ }
- cache.SetLayer(0)
- tensor := context.FromFloats([]float32{1, 2, 3, 4, 5, 6, 7}, 1, 1, 7)
- cache.Put(context, tensor, tensor)
-
- // shift window by adding position 7
- err = cache.StartForward(context, input.Batch{
- Positions: []int32{7},
- Sequences: []int{0},
- }, false)
- if err != nil {
- t.Fatalf("StartForward failed: %v", err)
- }
+ cache.SetLayer(0)
+ tensor := context.FromFloats([]float32{1, 2, 3, 4, 5, 6, 7}, 1, 1, 7)
+ cache.Put(context, tensor, tensor)
+
+ // shift window by adding position 7
+ err = cache.StartForward(context, input.Batch{
+ Positions: []int32{7},
+ Sequences: []int{0},
+ }, false)
+ if err != nil {
+ t.Fatalf("StartForward failed: %v", err)
+ }
- cache.SetLayer(0)
- tensor = context.FromFloats([]float32{8}, 1, 1, 1)
- cache.Put(context, tensor, tensor)
+ cache.SetLayer(0)
+ tensor = context.FromFloats([]float32{8}, 1, 1, 1)
+ cache.Put(context, tensor, tensor)
- // only the latest position has overlapping windows
- if cache.CanResume(0, 0) {
- t.Errorf("after shift: CanResume(0, 0) = true, want false (outside window)")
- }
- if cache.CanResume(0, 1) {
- t.Errorf("after shift: CanResume(0, 1) = true, want false (outside window)")
- }
- if cache.CanResume(0, 2) {
- t.Errorf("after shift: CanResume(0, 2) = true, want false (outside window)")
- }
- if cache.CanResume(0, 3) {
- t.Errorf("after shift: CanResume(0, 3) = true, want false (outside window)")
- }
- if cache.CanResume(0, 4) {
- t.Errorf("after shift: CanResume(0, 4) = true, want false (outside window)")
- }
- if cache.CanResume(0, 5) {
- t.Errorf("after shift: CanResume(0, 5) = true, want false (outside window)")
- }
- if !cache.CanResume(0, 6) {
- t.Errorf("after shift: CanResume(0, 6) = false, want true (inside window)")
- }
- if !cache.CanResume(0, 7) {
- t.Errorf("after shift: CanResume(0, 7) = false, want true (latest position)")
- }
+ // only the latest position has overlapping windows
+ if cache.CanResume(0, 0) {
+ t.Errorf("after shift: CanResume(0, 0) = true, want false (outside window)")
+ }
+ if cache.CanResume(0, 1) {
+ t.Errorf("after shift: CanResume(0, 1) = true, want false (outside window)")
+ }
+ if cache.CanResume(0, 2) {
+ t.Errorf("after shift: CanResume(0, 2) = true, want false (outside window)")
+ }
+ if cache.CanResume(0, 3) {
+ t.Errorf("after shift: CanResume(0, 3) = true, want false (outside window)")
+ }
+ if cache.CanResume(0, 4) {
+ t.Errorf("after shift: CanResume(0, 4) = true, want false (outside window)")
+ }
+ if cache.CanResume(0, 5) {
+ t.Errorf("after shift: CanResume(0, 5) = true, want false (outside window)")
+ }
+ if !cache.CanResume(0, 6) {
+ t.Errorf("after shift: CanResume(0, 6) = false, want true (inside window)")
+ }
+ if !cache.CanResume(0, 7) {
+ t.Errorf("after shift: CanResume(0, 7) = false, want true (latest position)")
+ }
+ })
}
type testBackend struct {
ml.Backend
+ permutedV bool
}
func (b *testBackend) NewContext() ml.Context {
@@ -649,6 +633,10 @@ func (b *testBackend) NewContextSize(int) ml.Context {
return &testContext{}
}
+func (b *testBackend) CacheConfig() ml.CacheConfig {
+ return ml.CacheConfig{PermutedV: b.permutedV}
+}
+
type testContext struct {
ml.Context
}
@@ -770,6 +758,15 @@ func (t *testTensor) Add(ctx ml.Context, t2 ml.Tensor) ml.Tensor {
return out
}
+func (t *testTensor) Reshape(ctx ml.Context, shape ...int) ml.Tensor {
+ return &testTensor{
+ dtype: t.dtype,
+ elementSize: t.elementSize,
+ data: t.data,
+ shape: shape,
+ }
+}
+
func (t *testTensor) View(ctx ml.Context, offset int, shape ...int) ml.Tensor {
offset /= t.elementSize
@@ -778,6 +775,8 @@ func (t *testTensor) View(ctx ml.Context, offset int, shape ...int) ml.Tensor {
switch len(shape) {
case 1:
s = []int{shape[0]}
+ case 3:
+ s = []int{shape[0], shape[2]}
case 5:
s = []int{shape[0], shape[2], shape[4]}
default:
@@ -792,6 +791,182 @@ func (t *testTensor) View(ctx ml.Context, offset int, shape ...int) ml.Tensor {
return view
}
+func (t *testTensor) Permute(ctx ml.Context, order ...int) ml.Tensor {
+ if len(t.shape) > 4 || len(order) > 4 {
+ panic("permute only supports up to 4 dimensions")
+ }
+
+ if len(order) != len(t.shape) && len(order) != 4 {
+ panic("invalid number of dimensions for permute")
+ }
+
+ // ggml_permute expects 4 axes, so fill in any missing dimensions.
+ orderFull := append(make([]int, 0, 4), order...)
+ for len(orderFull) < 4 {
+ orderFull = append(orderFull, len(orderFull))
+ }
+
+ seen := [4]bool{}
+
+ shape4 := [4]int{1, 1, 1, 1}
+ for i := 0; i < len(t.shape) && i < 4; i++ {
+ shape4[i] = t.shape[i]
+ }
+
+ newShape4 := [4]int{1, 1, 1, 1}
+ for axis := range 4 {
+ dst := orderFull[axis]
+ if dst < 0 || dst >= 4 {
+ panic("invalid axis for permute")
+ }
+ if seen[dst] {
+ panic("duplicate axis for permute")
+ }
+ seen[dst] = true
+ newShape4[dst] = shape4[axis]
+ }
+
+ total := len(t.data)
+ newData := make([]float32, total)
+
+ if total > 0 {
+ oldDims := shape4
+ newDims := newShape4
+
+ oldStride := [4]int{1, 1, 1, 1}
+ newStride := [4]int{1, 1, 1, 1}
+ for i := 1; i < 4; i++ {
+ oldStride[i] = oldStride[i-1] * oldDims[i-1]
+ newStride[i] = newStride[i-1] * newDims[i-1]
+ }
+
+ var coords [4]int
+ var newCoords [4]int
+
+ for idx := range total {
+ remainder := idx
+ for axis := range 4 {
+ dim := oldDims[axis]
+ if dim == 0 {
+ coords[axis] = 0
+ continue
+ }
+ coords[axis] = remainder % dim
+ remainder /= dim
+ }
+
+ for axis := range 4 {
+ newCoords[orderFull[axis]] = coords[axis]
+ }
+
+ newIndex := 0
+ for axis := range 4 {
+ if newDims[axis] == 0 {
+ continue
+ }
+ newIndex += newCoords[axis] * newStride[axis]
+ }
+
+ newData[newIndex] = t.data[idx]
+ }
+ }
+
+ numDims := 4
+ for numDims > 1 && newShape4[numDims-1] <= 1 {
+ numDims--
+ }
+
+ newShape := make([]int, numDims)
+ copy(newShape, newShape4[:numDims])
+
+ return &testTensor{
+ dtype: t.dtype,
+ elementSize: t.elementSize,
+ data: newData,
+ shape: newShape,
+ }
+}
+
+func (t *testTensor) SetRows(ctx ml.Context, src ml.Tensor, idxs ml.Tensor) ml.Tensor {
+ dst := t
+ srcTensor := src.(*testTensor)
+ idxTensor := idxs.(*testTensor)
+
+ shapeTo4D := func(shape []int) [4]int {
+ out := [4]int{1, 1, 1, 1}
+ for i := 0; i < len(shape) && i < 4; i++ {
+ out[i] = shape[i]
+ }
+ return out
+ }
+
+ computeStrides := func(shape [4]int) [4]int {
+ out := [4]int{1, 1, 1, 1}
+ for i := 1; i < 4; i++ {
+ out[i] = out[i-1] * shape[i-1]
+ }
+ return out
+ }
+
+ dstShape4D := shapeTo4D(dst.shape)
+ srcShape4D := shapeTo4D(srcTensor.shape)
+ idxShape4D := shapeTo4D(idxTensor.shape)
+
+ if dstShape4D[0] != srcShape4D[0] || dstShape4D[2] != srcShape4D[2] || dstShape4D[3] != srcShape4D[3] {
+ panic("SetRows requires matching tensor shapes")
+ }
+
+ if srcShape4D[1] != idxShape4D[0] {
+ panic("SetRows rows/index mismatch")
+ }
+
+ if srcShape4D[2]%idxShape4D[1] != 0 || srcShape4D[3]%idxShape4D[2] != 0 {
+ panic("SetRows cannot broadcast indices")
+ }
+
+ if idxShape4D[3] != 1 {
+ panic("SetRows expects 1D or 2D index tensors")
+ }
+
+ dstStride := computeStrides(dstShape4D)
+ srcStride := computeStrides(srcShape4D)
+ idxStride := computeStrides(idxShape4D)
+
+ numColumns := srcShape4D[0]
+ numRows := srcShape4D[1]
+
+ for dim3Index := range dstShape4D[3] {
+ for dim2Index := range dstShape4D[2] {
+ idxDim2 := 0
+ idxDim3 := 0
+ if idxShape4D[1] > 0 {
+ idxDim2 = dim2Index % idxShape4D[1]
+ }
+ if idxShape4D[2] > 0 {
+ idxDim3 = dim3Index % idxShape4D[2]
+ }
+
+ idxBase := idxDim3*idxStride[2] + idxDim2*idxStride[1]
+ srcBase := dim3Index*srcStride[3] + dim2Index*srcStride[2]
+ dstBase := dim3Index*dstStride[3] + dim2Index*dstStride[2]
+
+ for row := range numRows {
+ idx := int(idxTensor.data[idxBase+row*idxStride[0]])
+ if idx < 0 || idx >= dstShape4D[1] {
+ panic("SetRows index out of range")
+ }
+
+ srcOffset := srcBase + row*srcStride[1]
+ dstOffset := dstBase + idx*dstStride[1]
+
+ copy(dst.data[dstOffset:dstOffset+numColumns], srcTensor.data[srcOffset:srcOffset+numColumns])
+ }
+ }
+ }
+
+ return dst
+}
+
func (t *testTensor) Copy(ctx ml.Context, t2 ml.Tensor) ml.Tensor {
copy(t2.(*testTensor).data, t.data)
return nil
diff --git a/llama/build-info.cpp b/llama/build-info.cpp
index 7f5e28c7f03..b37cd25efad 100644
--- a/llama/build-info.cpp
+++ b/llama/build-info.cpp
@@ -1,4 +1,4 @@
int LLAMA_BUILD_NUMBER = 0;
-char const *LLAMA_COMMIT = "3cfa9c3f125763305b4226bc032f1954f08990dc";
+char const *LLAMA_COMMIT = "ec98e2002";
char const *LLAMA_COMPILER = "";
char const *LLAMA_BUILD_TARGET = "";
diff --git a/llama/llama.cpp/.rsync-filter b/llama/llama.cpp/.rsync-filter
index 650d9463bf8..7987be1c83d 100644
--- a/llama/llama.cpp/.rsync-filter
+++ b/llama/llama.cpp/.rsync-filter
@@ -17,11 +17,17 @@ include /tools/mtmd/clip.cpp
include /tools/mtmd/mtmd.cpp
include /tools/mtmd/mtmd-audio.cpp
include /tools/mtmd/mtmd-helper.cpp
+include /tools/mtmd/models/
+include /tools/mtmd/models/*.h
+include /tools/mtmd/models/*.cpp
include /src/
include /src/llama.*
include /src/llama-*.*
include /src/unicode-data.*
include /src/unicode.*
+include /src/models/
+include /src/models/*.h
+include /src/models/*.cpp
include /vendor/
include /vendor/miniaudio/
include /vendor/miniaudio/*.h
diff --git a/llama/llama.cpp/common/common.cpp b/llama/llama.cpp/common/common.cpp
index b0591e84b06..5a8cf524856 100644
--- a/llama/llama.cpp/common/common.cpp
+++ b/llama/llama.cpp/common/common.cpp
@@ -8,6 +8,7 @@
#include "common.h"
#include "log.h"
#include "llama.h"
+#include "sampling.h"
#include
#include
@@ -26,7 +27,6 @@
#include
#include
#include
-#include
#include
#include
@@ -60,6 +60,14 @@
#pragma warning(disable: 4244 4267) // possible loss of data
#endif
+common_time_meas::common_time_meas(int64_t & t_acc, bool disable) : t_start_us(disable ? -1 : ggml_time_us()), t_acc(t_acc) {}
+
+common_time_meas::~common_time_meas() {
+ if (t_start_us >= 0) {
+ t_acc += ggml_time_us() - t_start_us;
+ }
+}
+
//
// CPU utils
//
@@ -355,11 +363,7 @@ bool parse_cpu_mask(const std::string & mask, bool (&boolmask)[GGML_MAX_N_THREAD
}
void common_init() {
- llama_log_set([](ggml_log_level level, const char * text, void * /*user_data*/) {
- if (LOG_DEFAULT_LLAMA <= common_log_verbosity_thold) {
- common_log_add(common_log_main(), level, "%s", text);
- }
- }, NULL);
+ llama_log_set(common_log_default_callback, NULL);
#ifdef NDEBUG
const char * build_type = "";
@@ -690,7 +694,7 @@ bool string_parse_kv_override(const char * data, std::vector= 0xD800 && c <= 0xDFFF) // UTF-16 surrogate pairs
|| c == 0xFFFD // Replacement Character (UTF-8)
|| c == 0xFEFF // Byte Order Mark (BOM)
- || c == '/' || c == '\\' || c == ':' || c == '*' // Illegal characters
+ || c == ':' || c == '*' // Illegal characters
|| c == '?' || c == '"' || c == '<' || c == '>' || c == '|') {
return false;
}
+ if (!allow_subdirs && (c == '/' || c == '\\')) {
+ // Subdirectories not allowed, reject path separators
+ return false;
+ }
}
// Reject any leading or trailing ' ', or any trailing '.', these are stripped on Windows and will cause a different filename
@@ -778,11 +786,29 @@ bool fs_validate_filename(const std::string & filename) {
#include
+#ifdef _WIN32
+static std::wstring utf8_to_wstring(const std::string & str) {
+ if (str.empty()) {
+ return std::wstring();
+ }
+
+ int size = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), NULL, 0);
+
+ if (size <= 0) {
+ return std::wstring();
+ }
+
+ std::wstring wstr(size, 0);
+ MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), &wstr[0], size);
+
+ return wstr;
+}
+#endif
+
// returns true if successful, false otherwise
bool fs_create_directory_with_parents(const std::string & path) {
#ifdef _WIN32
- std::wstring_convert> converter;
- std::wstring wpath = converter.from_bytes(path);
+ std::wstring wpath = utf8_to_wstring(path);
// if the path already exists, check whether it's a directory
const DWORD attributes = GetFileAttributesW(wpath.c_str());
@@ -855,6 +881,11 @@ bool fs_create_directory_with_parents(const std::string & path) {
#endif // _WIN32
}
+bool fs_is_directory(const std::string & path) {
+ std::filesystem::path dir(path);
+ return std::filesystem::exists(dir) && std::filesystem::is_directory(dir);
+}
+
std::string fs_get_cache_directory() {
std::string cache_directory = "";
auto ensure_trailing_slash = [](std::string p) {
@@ -889,6 +920,8 @@ std::string fs_get_cache_directory() {
cache_directory = std::getenv("HOME") + std::string("/Library/Caches/");
#elif defined(_WIN32)
cache_directory = std::getenv("LOCALAPPDATA");
+#elif defined(__EMSCRIPTEN__)
+ GGML_ABORT("not implemented on this platform");
#else
# error Unknown architecture
#endif
@@ -908,34 +941,258 @@ std::string fs_get_cache_file(const std::string & filename) {
return cache_directory + filename;
}
+std::vector fs_list(const std::string & path, bool include_directories) {
+ std::vector files;
+ if (path.empty()) return files;
+
+ std::filesystem::path dir(path);
+ if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) {
+ return files;
+ }
+
+ for (const auto & entry : std::filesystem::directory_iterator(dir)) {
+ try {
+ // Only include regular files (skip directories)
+ const auto & p = entry.path();
+ if (std::filesystem::is_regular_file(p)) {
+ common_file_info info;
+ info.path = p.string();
+ info.name = p.filename().string();
+ info.is_dir = false;
+ try {
+ info.size = static_cast(std::filesystem::file_size(p));
+ } catch (const std::filesystem::filesystem_error &) {
+ info.size = 0;
+ }
+ files.push_back(std::move(info));
+ } else if (include_directories && std::filesystem::is_directory(p)) {
+ common_file_info info;
+ info.path = p.string();
+ info.name = p.filename().string();
+ info.size = 0; // Directories have no size
+ info.is_dir = true;
+ files.push_back(std::move(info));
+ }
+ } catch (const std::filesystem::filesystem_error &) {
+ // skip entries we cannot inspect
+ continue;
+ }
+ }
+
+ return files;
+}
+
+//
+// TTY utils
+//
+
+bool tty_can_use_colors() {
+ // Check NO_COLOR environment variable (https://no-color.org/)
+ if (const char * no_color = std::getenv("NO_COLOR")) {
+ if (no_color[0] != '\0') {
+ return false;
+ }
+ }
+
+ // Check TERM environment variable
+ if (const char * term = std::getenv("TERM")) {
+ if (std::strcmp(term, "dumb") == 0) {
+ return false;
+ }
+ }
+
+ // Check if stdout and stderr are connected to a terminal
+ // We check both because log messages can go to either
+ bool stdout_is_tty = isatty(fileno(stdout));
+ bool stderr_is_tty = isatty(fileno(stderr));
+
+ return stdout_is_tty || stderr_is_tty;
+}
//
// Model utils
//
-struct common_init_result common_init_from_params(common_params & params) {
- common_init_result iparams;
+// TODO: move to common/sampling
+static void common_init_sampler_from_model(
+ const llama_model * model,
+ common_params_sampling & sparams) {
+
+ const uint64_t config = sparams.user_sampling_config;
+
+ auto get_int32 = [&](const char * key, int32_t & dst, uint64_t user_config) {
+ if (config & user_config) {
+ return;
+ }
+
+ char buf[64] = {0};
+ if (llama_model_meta_val_str(model, key, buf, sizeof(buf)) > 0) {
+ char * end = nullptr;
+ int32_t v = strtol(buf, &end, 10);
+ if (end && end != buf) {
+ dst = v;
+ }
+ }
+ };
+
+ auto get_float = [&](const char * key, float & dst, uint64_t user_config) {
+ if (config & user_config) {
+ return;
+ }
+
+ char buf[128] = {0};
+ if (llama_model_meta_val_str(model, key, buf, sizeof(buf)) > 0) {
+ char * end = nullptr;
+ float v = strtof(buf, &end);
+ if (end && end != buf) {
+ dst = v;
+ }
+ }
+ };
+
+ // Sampling sequence
+ if (!(config & common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_SAMPLERS)) {
+ char buf[512] = {0};
+ if (llama_model_meta_val_str(model, llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_SEQUENCE), buf, sizeof(buf)) > 0) {
+ const std::vector sampler_names = string_split(std::string(buf), ';');
+ if (!sampler_names.empty()) {
+ sparams.samplers = common_sampler_types_from_names(sampler_names, true);
+ }
+ }
+ }
+
+ get_int32(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_TOP_K), sparams.top_k, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_TOP_K);
+ get_float(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_TOP_P), sparams.top_p, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_TOP_P);
+ get_float(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_MIN_P), sparams.min_p, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_MIN_P);
+ get_float(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_XTC_PROBABILITY), sparams.xtc_probability, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_XTC_PROBABILITY);
+ get_float(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_XTC_THRESHOLD), sparams.xtc_threshold, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_XTC_THRESHOLD);
+ get_float(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_TEMP), sparams.temp, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_TEMP);
+ get_int32(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_PENALTY_LAST_N), sparams.penalty_last_n, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_PENALTY_LAST_N);
+ get_float(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_PENALTY_REPEAT), sparams.penalty_repeat, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_PENALTY_REPEAT);
+ get_int32(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_MIROSTAT), sparams.mirostat, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_MIROSTAT);
+ get_float(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_MIROSTAT_TAU), sparams.mirostat_tau, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_MIROSTAT_TAU);
+ get_float(llama_model_meta_key_str(LLAMA_MODEL_META_KEY_SAMPLING_MIROSTAT_ETA), sparams.mirostat_eta, common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_MIROSTAT_ETA);
+}
+
+struct common_init_result::impl {
+ impl() = default;
+ ~impl() = default;
+
+ llama_model_ptr model;
+ llama_context_ptr context;
+
+ std::vector lora;
+
+ std::vector samplers;
+};
+
+common_init_result::common_init_result(common_params & params) :
+ pimpl(new impl{}) {
auto mparams = common_model_params_to_llama(params);
+ auto cparams = common_context_params_to_llama(params);
+
+ if (params.fit_params) {
+ LOG_INF("%s: fitting params to device memory, to report bugs during this step use -fit off (or --verbose if you can't)\n", __func__);
+ llama_params_fit(params.model.path.c_str(), &mparams, &cparams,
+ params.tensor_split, params.tensor_buft_overrides.data(), params.fit_params_target, params.fit_params_min_ctx,
+ params.verbosity >= 4 ? GGML_LOG_LEVEL_DEBUG : GGML_LOG_LEVEL_ERROR);
+ }
llama_model * model = llama_model_load_from_file(params.model.path.c_str(), mparams);
if (model == NULL) {
- LOG_ERR("%s: failed to load model '%s', try reducing --n-gpu-layers if you're running out of VRAM\n",
- __func__, params.model.path.c_str());
- return iparams;
+ return;
}
+ pimpl->model.reset(model);
+
const llama_vocab * vocab = llama_model_get_vocab(model);
- auto cparams = common_context_params_to_llama(params);
+ // updates params.sampling
+ // TODO: fix naming
+ common_init_sampler_from_model(model, params.sampling);
+
+ if (params.sampling.ignore_eos && llama_vocab_eos(vocab) == LLAMA_TOKEN_NULL) {
+ LOG_WRN("%s: warning: vocab does not have an EOS token, ignoring --ignore-eos\n", __func__);
+ params.sampling.ignore_eos = false;
+ }
+
+ // initialize once
+ for (llama_token i = 0; i < llama_vocab_n_tokens(vocab); i++) {
+ if (llama_vocab_is_eog(vocab, i)) {
+ LOG_INF("%s: added %s logit bias = %f\n", __func__, common_token_to_piece(vocab, i).c_str(), -INFINITY);
+ params.sampling.logit_bias_eog.push_back({i, -INFINITY});
+ }
+ }
+
+ if (params.sampling.ignore_eos) {
+ // add EOG biases to the active set of logit biases
+ params.sampling.logit_bias.insert(
+ params.sampling.logit_bias.end(),
+ params.sampling.logit_bias_eog.begin(), params.sampling.logit_bias_eog.end());
+ }
+
+ //if (params.sampling.penalty_last_n == -1) {
+ // LOG_INF("%s: setting penalty_last_n to ctx_size = %d\n", __func__, llama_n_ctx(lctx));
+ // params.sampling.penalty_last_n = llama_n_ctx(lctx);
+ //}
+
+ //if (params.sampling.dry_penalty_last_n == -1) {
+ // LOG_INF("%s: setting dry_penalty_last_n to ctx_size = %d\n", __func__, llama_n_ctx(lctx));
+ // params.sampling.dry_penalty_last_n = llama_n_ctx(lctx);
+ //}
+
+ pimpl->samplers.resize(cparams.n_seq_max);
+
+ for (int i = 0; i < (int) cparams.n_seq_max; ++i) {
+ pimpl->samplers[i].reset(common_sampler_init(model, params.sampling));
+ }
llama_context * lctx = llama_init_from_model(model, cparams);
if (lctx == NULL) {
- LOG_ERR("%s: failed to create context with model '%s', try reducing --n-gpu-layers if you're running out of VRAM\n",
- __func__, params.model.path.c_str());
- llama_model_free(model);
- return iparams;
+ LOG_ERR("%s: failed to create context with model '%s'\n", __func__, params.model.path.c_str());
+ return;
+ }
+
+ pimpl->context.reset(lctx);
+}
+
+llama_model * common_init_result::model() {
+ return pimpl->model.get();
+}
+
+llama_context * common_init_result::context() {
+ return pimpl->context.get();
+}
+
+common_sampler * common_init_result::sampler(llama_seq_id seq_id) {
+ return pimpl->samplers[seq_id].get();
+}
+
+std::vector & common_init_result::lora() {
+ return pimpl->lora;
+}
+
+void common_init_result::free_context() {
+ pimpl->context.reset();
+}
+
+common_init_result_ptr common_init_from_params(common_params & params) {
+ common_init_result_ptr res(new common_init_result(params));
+
+ llama_model * model = res->model();
+ if (model == NULL) {
+ LOG_ERR("%s: failed to load model '%s'\n", __func__, params.model.path.c_str());
+ return res;
+ }
+
+ llama_context * lctx = res->context();
+ if (lctx == NULL) {
+ LOG_ERR("%s: failed to create context with model '%s'\n", __func__, params.model.path.c_str());
+ return res;
}
+ const llama_vocab * vocab = llama_model_get_vocab(model);
+
if (params.ctx_shift && !llama_memory_can_shift(llama_get_memory(lctx))) {
LOG_WRN("%s: KV cache shifting is not supported for this context, disabling KV cache shifting\n", __func__);
params.ctx_shift = false;
@@ -947,10 +1204,7 @@ struct common_init_result common_init_from_params(common_params & params) {
const auto cvec = common_control_vector_load(params.control_vectors);
if (cvec.n_embd == -1) {
- llama_free(lctx);
- llama_model_free(model);
-
- return iparams;
+ return res;
}
int err = llama_apply_adapter_cvec(
@@ -961,10 +1215,7 @@ struct common_init_result common_init_from_params(common_params & params) {
params.control_vector_layer_start,
params.control_vector_layer_end);
if (err) {
- llama_free(lctx);
- llama_model_free(model);
-
- return iparams;
+ return res;
}
}
@@ -988,10 +1239,7 @@ struct common_init_result common_init_from_params(common_params & params) {
}
if (!ok) {
- llama_free(lctx);
- llama_model_free(model);
-
- return iparams;
+ return res;
}
}
@@ -1001,9 +1249,7 @@ struct common_init_result common_init_from_params(common_params & params) {
lora.reset(llama_adapter_lora_init(model, la.path.c_str()));
if (lora == nullptr) {
LOG_ERR("%s: failed to apply lora adapter '%s'\n", __func__, la.path.c_str());
- llama_free(lctx);
- llama_model_free(model);
- return iparams;
+ return res;
}
char buf[1024];
@@ -1012,43 +1258,13 @@ struct common_init_result common_init_from_params(common_params & params) {
la.task_name = buf;
llama_adapter_meta_val_str(la.ptr, "adapter.lora.prompt_prefix", buf, sizeof(buf));
la.prompt_prefix = buf;
- iparams.lora.emplace_back(std::move(lora)); // copy to list of loaded adapters
+ res->lora().emplace_back(std::move(lora)); // copy to list of loaded adapters
}
if (!params.lora_init_without_apply) {
common_set_adapter_lora(lctx, params.lora_adapters);
}
- if (params.sampling.ignore_eos && llama_vocab_eos(vocab) == LLAMA_TOKEN_NULL) {
- LOG_WRN("%s: warning: vocab does not have an EOS token, ignoring --ignore-eos\n", __func__);
- params.sampling.ignore_eos = false;
- }
-
- // initialize once
- for (llama_token i = 0; i < llama_vocab_n_tokens(vocab); i++) {
- if (llama_vocab_is_eog(vocab, i)) {
- LOG_INF("%s: added %s logit bias = %f\n", __func__, common_token_to_piece(lctx, i).c_str(), -INFINITY);
- params.sampling.logit_bias_eog.push_back({i, -INFINITY});
- }
- }
-
- if (params.sampling.ignore_eos) {
- // add EOG biases to the active set of logit biases
- params.sampling.logit_bias.insert(
- params.sampling.logit_bias.end(),
- params.sampling.logit_bias_eog.begin(), params.sampling.logit_bias_eog.end());
- }
-
- if (params.sampling.penalty_last_n == -1) {
- LOG_INF("%s: setting penalty_last_n to ctx_size = %d\n", __func__, llama_n_ctx(lctx));
- params.sampling.penalty_last_n = llama_n_ctx(lctx);
- }
-
- if (params.sampling.dry_penalty_last_n == -1) {
- LOG_INF("%s: setting dry_penalty_last_n to ctx_size = %d\n", __func__, llama_n_ctx(lctx));
- params.sampling.dry_penalty_last_n = llama_n_ctx(lctx);
- }
-
if (params.warmup) {
LOG_WRN("%s: warming up the model with an empty run - please wait ... (--no-warmup to disable)\n", __func__);
@@ -1087,12 +1303,11 @@ struct common_init_result common_init_from_params(common_params & params) {
llama_set_warmup(lctx, false);
}
- iparams.model.reset(model);
- iparams.context.reset(lctx);
-
- return iparams;
+ return res;
}
+common_init_result::~common_init_result() = default;
+
std::string get_model_endpoint() {
const char * model_endpoint_env = getenv("MODEL_ENDPOINT");
// We still respect the use of environment-variable "HF_ENDPOINT" for backward-compatibility.
@@ -1101,7 +1316,9 @@ std::string get_model_endpoint() {
std::string model_endpoint = "https://huggingface.co/";
if (endpoint_env) {
model_endpoint = endpoint_env;
- if (model_endpoint.back() != '/') model_endpoint += '/';
+ if (model_endpoint.back() != '/') {
+ model_endpoint += '/';
+ }
}
return model_endpoint;
}
diff --git a/llama/llama.cpp/common/common.h b/llama/llama.cpp/common/common.h
index a8cb630ea58..d70744840fb 100644
--- a/llama/llama.cpp/common/common.h
+++ b/llama/llama.cpp/common/common.h
@@ -2,17 +2,19 @@
#pragma once
+#include "ggml-opt.h"
+#include "llama-cpp.h"
+
#include
#include
#include
#include
#include
#include