Using Open source AI in my local machine
Open source AI is the gold standard for public AI thought as a public good. Apertus is an open source AI developed by the Swiss AI Initiative. The question is: how do you use it in your own machine?
The LLM Apertus is published in Swiss AI Initiative hugging face profile. As of today, its latest version is 1.5 and comes in two sizes (8B and 70B). 70B is hard to fit in consumer hardware (does not fit in mine), so we will go for Apertus v1.5 8B.
As it stands, the recommended setup to perform inference is using vLLM. Unfortunately, vLLM is designed for large scale deployment, not single usage on a laptop, so it is not the best deployment tool for us. Instead, the best tool for personal local usage is llama.cpp.
Moreover, Apertus v1.5 is multimodal, accepting text, audio, and visual input. For now, we simply want to chat with it, so text only. Therefore, we will use a stripped down version Apertus v1.5 8B for text-only. Lastly, to perform inference with llama.cpp instead of vLLM, we need the model in the GGUF format (instead of the published safetensors format). Therefore, we will use andreasmartin/apertus-v1.5-8b-text-Q8_0-GGUF.
Here is the simplest interface that works for me:
- Download apertus-v1.5-8b-text-q8_0.gguf
- Install llama.cpp. For example by running
brew install llama.cpp. - Run a server performing inference on the model by running
llama-server --alias apertus-1.5-8b --jinja --ctx-size 16384 --parallel 1 --threads 8 --threads-batch 16 --flash-attn on --cache-type-k q8_0 --cache-type-v q8_0 --cache-reuse 256 --temp 0.8 --top-p 0.9 --host 0.0.0.0 --port 8080 --reasoning off --model path/to/apertus-v1.5-8b-text-q8_0.ggufIf you are willing to go a bit further, there are some improvements you can do to this setup. For now, this uses the following options.
| Option | What it does | Default | Your value |
|---|---|---|---|
--model |
Path to the GGUF file | none (required) | the Apertus Q8_0 file |
--alias |
Model name shown in the UI and returned by /v1/models |
derived from the model path | apertus-1.5-8b |
--jinja |
Formats chat messages with the Jinja chat template embedded in the GGUF | on in recent builds, off in older ones | on (safe to keep either way) |
--ctx-size |
Context window in tokens; sets how much KV-cache memory is reserved | older builds: 4096; newer builds: 0, meaning the model’s full training context (262K here) | 16384 |
--parallel |
Number of simultaneous request slots, which share the context | auto (−1) in recent builds, often 4; 1 in older builds | 1, so one conversation gets all 16K |
--threads |
CPU threads for token generation | auto, roughly the physical core count | 8 |
--threads-batch |
CPU threads for prompt processing | same as --threads |
16 |
--flash-attn |
Flash attention: faster, uses less memory, and is required for a quantized V cache | auto |
on |
--cache-type-k |
Precision of the K half of the KV cache | f16 |
q8_0 (half the memory) |
--cache-type-v |
Precision of the V half of the KV cache | f16 |
q8_0 |
--cache-reuse |
Minimum matching chunk size (in tokens) for reusing cached prompt parts via KV shifting; speeds up edits and regenerations | 0 (off) | 256 |
--temp |
Sampling temperature | 0.8 | 0.8 (already the default) |
--top-p |
Nucleus sampling cutoff | 0.95 | 0.9 (Swiss AI’s recommendation) |
--host |
Address the server listens on | 127.0.0.1 (localhost only) |
0.0.0.0 (all interfaces, needed for Tailscale) |
--port |
HTTP port | 8080 | 8080 (already the default) |
--reasoning |
Thinking mode | auto | on |
Warning: If, for some reason, your llama.cpp installation does not include a UI, then you have to do the following.
- Run this to download the llama UI
mkdir path/to/llama-ui curl -L https://huggingface.co/buckets/ggml-org/llama-ui/resolve/latest/dist.tar.gz | tar -xz -C path/to/llama-ui - Add
--path path/to/llama-uito the llama-server call
Custom chat template
To get the best experience, you will need to change the “chat template”. Until now, we have used the original chat template from Swiss AI, which describes a multimodal model, and is tested on a different inference engine, not llama.cpp. Therefore, there are a few errors that can be easily avoided.
Errors considered
Default system prompt
- Describes a multimodal model. But this deployment handles text only.
Reasoning (thinking) is part of the response
- Reasoning appears together with the response.
- Past reasoning becomes part of the chat history.
Cache does not handle reasoning correctly
- Reasoning is not saved for caching by default.
- But now that reasoning is part of the response, the cache will miss on the latest reasoning step
Proposal
Default system prompt
You are Apertus 1.5, an assistant developed by the Swiss AI Initiative, extended from Apertus 1 via continued pretraining. In this deployment you handle text only.
Reasoning display and cache friendly
By adding a few lines to the chat template, the reasoning is displayed correctly, and taken into account for the cache.
To dissable including the reasoning on the cache, add --no-reasoning-preserve on the command.
Now, the command to start the server is the following.
llama-server --alias apertus-1.5-8b --jinja --ctx-size 16384 --parallel 1 --threads 8 --threads-batch 16 --flash-attn on --cache-type-k q8_0 --cache-type-v q8_0 --cache-reuse 256 --temp 0.8 --top-p 0.9 --host 0.0.0.0 --port 8080 --reasoning on --model path/to/apertus-v1.5-8b-text-q8_0.gguf --path path/to/llama-ui --chat-template-file path/to/apertus-v1.5-8b-text-q8_0-chat_template.jinja
Below is the final chat_template. I use it as the file apertus-v1.5-8b-text-q8_0-chat_template.jinja.
{%- macro render_typescript_type(param_spec, required_params, is_nullable=false) -%}
{%- if param_spec.type == "array" -%}
{%- if param_spec['items'] -%}
{%- if param_spec['items']['type'] == "string" -%}
{{- "string[]" }}
{%- elif param_spec['items']['type'] == "number" -%}
{{- "number[]" }}
{%- elif param_spec['items']['type'] == "integer" -%}
{{- "number[]" }}
{%- elif param_spec['items']['type'] == "boolean" -%}
{{- "boolean[]" }}
{%- else -%}
{%- set inner_type = render_typescript_type(param_spec['items'], required_params) -%}
{%- if inner_type == "object | object" or inner_type|length > 50 -%}
{{- "any[]" }}
{%- else -%}
{{- inner_type + "[]" }}
{%- endif -%}
{%- endif -%}
{%- if param_spec.nullable -%}
{{- " | null" }}
{%- endif -%}
{%- else -%}
{{- "any[]" }}
{%- if param_spec.nullable -%}
{{- " | null" }}
{%- endif -%}
{%- endif -%}
{%- elif param_spec.type is defined and param_spec.type is iterable and param_spec.type is not string and param_spec.type is not mapping and param_spec.type[0] is defined -%}
{#- Handle array of types like ["object", "object"] from Union[dict, list] #}
{%- if param_spec.type | length > 1 -%}
{{- param_spec.type | join(" | ") }}
{%- else -%}
{{- param_spec.type[0] }}
{%- endif -%}
{%- elif param_spec.oneOf -%}
{#- Handle oneOf schemas - check for complex unions and fallback to any #}
{%- set has_object_variants = false -%}
{%- for variant in param_spec.oneOf -%}
{%- if variant.type == "object" -%}
{%- set has_object_variants = true -%}
{%- endif -%}
{%- endfor -%}
{%- if has_object_variants and param_spec.oneOf|length > 1 -%}
{{- "any" }}
{%- else -%}
{%- for variant in param_spec.oneOf -%}
{{- render_typescript_type(variant, required_params) -}}
{%- if variant.description %}
{{- "// " + variant.description }}
{%- endif -%}
{%- if variant.default is defined %}
{{ "// default: " + variant.default|tojson }}
{%- endif -%}
{%- if not loop.last %}
{{- " | " }}
{% endif -%}
{%- endfor -%}
{%- endif -%}
{%- elif param_spec.type == "string" -%}
{%- if param_spec.enum -%}
{{- '"' + param_spec.enum|join('" | "') + '"' -}}
{%- else -%}
{{- "string" }}
{%- if param_spec.nullable %}
{{- " | null" }}
{%- endif -%}
{%- endif -%}
{%- elif param_spec.type == "number" -%}
{{- "number" }}
{%- elif param_spec.type == "integer" -%}
{{- "number" }}
{%- elif param_spec.type == "boolean" -%}
{{- "boolean" }}
{%- elif param_spec.type == "object" -%}
{%- if param_spec.properties -%}
{{- "{\n" }}
{%- for prop_name, prop_spec in param_spec.properties.items() -%}
{{- prop_name -}}
{%- if prop_name not in (param_spec.required or []) -%}
{{- "?" }}
{%- endif -%}
{{- ": " }}
{{ render_typescript_type(prop_spec, param_spec.required or []) }}
{%- if not loop.last -%}
{{-", " }}
{%- endif -%}
{%- endfor -%}
{{- "}" }}
{%- else -%}
{{- "object" }}
{%- endif -%}
{%- else -%}
{{- "any" }}
{%- endif -%}
{%- endmacro -%}
{%- macro render_tools(tools) -%}
{%- for tool in tools %}
{%- if tool.function %}
{%- set tool = tool.function %}
{%- endif %}
{{- "// " + tool.description + "\n" }}
{{- "type "+ tool.name + " = " }}
{%- if tool.parameters and tool.parameters.properties %}
{{- "(_: {\n" }}
{%- for param_name, param_spec in tool.parameters.properties.items() %}
{%- if param_spec.description %}
{{- "// " + param_spec.description + "\n" }}
{%- endif %}
{{- param_name }}
{%- if param_name not in (tool.parameters.required or []) -%}
{{- "?" }}
{%- endif -%}
{{- ": " }}
{{- render_typescript_type(param_spec, tool.parameters.required or []) }}
{%- if param_spec.default is defined -%}
{%- if param_spec.enum %}
{{- ", // default: " + param_spec.default }}
{%- elif param_spec.oneOf %}
{{- "// default: " + param_spec.default }}
{%- else %}
{{- ", // default: " + param_spec.default|tojson }}
{%- endif -%}
{%- endif -%}
{%- if not loop.last %}
{{- ",\n" }}
{%- else %}
{{- "\n" }}
{%- endif -%}
{%- endfor %}
{{- "}) => any;" }}
{%- else -%}
{{- "() => any;" }}
{%- endif -%}
{%- if not loop.last -%}
{{- "\n" }}
{%- endif -%}
{%- endfor %}
{%- endmacro -%}
{{ bos_token }}
{%- set system_token = '<|system_start|>' -%}
{%- set end_system_token = '<|system_end|>' -%}
{%- set developer_token = '<|developer_start|>' -%}
{%- set end_developer_token = '<|developer_end|>' -%}
{%- set user_token = '<|user_start|>' -%}
{%- set end_user_token = '<|user_end|>' -%}
{%- set assistant_token = '<|assistant_start|>' -%}
{%- set end_assistant_token = '<|assistant_end|>' -%}
{%- set inner_token = '<|inner_prefix|>' -%}
{%- set outer_token = '<|inner_suffix|>' -%}
{%- set tool_calls_token = '<|tools_prefix|>' -%}
{%- set end_tool_calls_token = '<|tools_suffix|>' -%}
{%- set tool_output_start_token = '<|tool_output_start|>' -%}
{%- set tool_output_end_token = '<|tool_output_end|>' -%}
{%- set image_token = '<|image|>' -%}
{%- set audio_token = '<|audio|>' -%}
{%- set ns = namespace(in_assistant=false, in_tool=false, in_inner=false, waiting_for_tool_outputs=false, assistant_format=none) -%}
{%- if messages and messages[0].role == 'system' -%}
{%- if "content" in messages[0] -%}
{%- if messages[0].content is string -%}
{{ system_token + messages[0].content + end_system_token }}
{%- elif messages[0].content is mapping and "text" in messages[0].content -%}
{{ system_token + messages[0].content.text + end_system_token }}
{%- else -%}
{{- raise_exception("Invalid system message") -}}
{%- endif -%}
{%- else -%}
{{- raise_exception("Invalid system message") -}}
{%- endif -%}
{%- set loop_messages = messages[1:] -%}
{%- else -%}
{{ system_token + 'You are Apertus 1.5, an assistant developed by the Swiss AI Initiative, extended from Apertus 1 via continued pretraining. In this deployment you handle text only.' + end_system_token }}
{%- set loop_messages = messages -%}
{%- endif -%}
{{ developer_token + 'Deliberation: ' }}
{%- if enable_thinking is defined and enable_thinking -%}
{{ 'enabled\n' }}
{%- else -%}
{{ 'disabled\n' }}
{%- endif -%}
{%- if tools is defined and tools -%}
{{ 'Tool Capabilities:\n' + render_tools(tools) }}
{%- else -%}
{{ 'Tool Capabilities: disabled' }}
{%- endif -%}
{{ end_developer_token }}
{%- for message in loop_messages -%}
{%- if message.role == 'user' -%}
{%- set ns.in_inner = false -%}
{%- if ns.in_tool -%}
{{ tool_output_end_token }}
{%- set ns.in_tool = false -%}
{%- endif -%}
{%- if ns.in_assistant -%}
{{ end_assistant_token }}
{%- set ns.in_assistant = false -%}
{%- endif -%}
{%- if "content" in message -%}
{{ user_token }}
{%- if message.content is string -%}
{{ message.content }}
{%- elif message.content is mapping and "parts" in message.content -%}
{%- set parts = message.content.parts -%}
{%- for part in parts -%}
{%- if part.type == "text" -%}
{{ part.text }}
{%- elif part.type == "image" or part.type == "image_url" or part.type == "input_image" -%}
{{ image_token }}
{%- elif part.type == "audio" or part.type == "audio_url" or part.type == "input_audio" -%}
{{ audio_token }}
{%- else -%}
{{- raise_exception("Invalid user part: " + part.type) -}}
{%- endif -%}
{%- endfor -%}
{%- elif message.content is not string and message.content is not mapping and message.content is iterable -%}
{%- for part in message.content -%}
{%- if part.type == "text" -%}
{{ part.text }}
{%- elif part.type == "image" or part.type == "image_url" or part.type == "input_image" -%}
{{ image_token }}
{%- elif part.type == "audio" or part.type == "audio_url" or part.type == "input_audio" -%}
{{ audio_token }}
{%- else -%}
{{- raise_exception("Invalid user part: " + part.type) -}}
{%- endif -%}
{%- endfor -%}
{%- else -%}
{{- raise_exception("Invalid user message: " + message.role) -}}
{%- endif -%}
{{ end_user_token }}
{%- endif -%}
{%- elif message.role == 'assistant' -%}
{%- if not ns.in_assistant -%}
{{ assistant_token }}
{%- set ns.in_assistant = true -%}
{%- endif -%}
{%- if "content" in message -%}
{%- if (message.content is string or message.content is none) and (ns.assistant_format is none or ns.assistant_format == "string") -%}
{%- if ns.in_tool -%}
{{ tool_output_end_token }}
{%- set ns.in_tool = false -%}
{%- endif -%}
{%- set ns.assistant_format = "string" -%}
{%- if message.reasoning_content is string and message.reasoning_content -%}
{{ inner_token + message.reasoning_content + outer_token }}
{%- endif -%}
{{ message.content or "" }}
{%- elif message.content is mapping and "blocks" in message.content and (ns.assistant_format is none or ns.assistant_format == "mapping") -%}
{%- set ns.assistant_format = "mapping" -%}
{%- set blocks = message.content.blocks -%}
{%- for block in blocks -%}
{%- if block.type == 'thoughts' -%}
{%- if ns.in_tool -%}
{{ tool_output_end_token }}
{%- set ns.in_tool = false -%}
{%- endif -%}
{%- if not ns.in_inner -%}
{%- set ns.in_inner = true -%}
{{ inner_token }}
{%- endif -%}
{{ block.text }}
{%- elif block.type == 'tool_calls' -%}
{%- if ns.in_tool -%}
{{ tool_output_end_token }}
{%- set ns.in_tool = false -%}
{%- endif -%}
{%- if ns.in_inner and not loop.first and block.calls|length == 1 and block.calls[0].name == 'display_answers' -%}
{%- set ns.in_inner = false -%}
{{ outer_token }}
{%- endif -%}
{{ tool_calls_token + '[' }}
{%- for tool_call in block.calls -%}
{%- if tool_call.function %}
{%- set tool_call = tool_call.function %}
{%- endif %}
{{- '{"' + tool_call.name + '": ' + (tool_call.arguments if tool_call.arguments is string else tool_call.arguments | tojson) + '}' }}
{%- if not loop.last -%}
{{- ", " }}
{%- endif -%}
{%- endfor -%}
{{ ']' + end_tool_calls_token }}
{%- set ns.waiting_for_tool_outputs = true -%}
{%- elif block.type == 'tool_outputs' -%}
{%- if ns.in_tool -%}
{{- raise_exception("Cannot have both tool outputs as separate messages and tool outputs as blocks") -}}
{%- endif -%}
{{ tool_output_start_token }}
{%- for tool_output in block.outputs -%}
{{- tool_output.output }}
{%- if not loop.last -%}
{{- ", " }}
{%- endif -%}
{%- endfor -%}
{{- tool_output_end_token }}
{%- set ns.waiting_for_tool_outputs = false -%}
{%- elif block.type == 'response' -%}
{%- if ns.in_tool -%}
{{ tool_output_end_token }}
{%- set ns.in_tool = false -%}
{%- endif -%}
{%- if (not loop.first and ns.in_inner) or (ns.in_assistant and ns.in_inner) -%}
{%- set ns.in_inner = false -%}
{{ outer_token }}
{%- endif -%}
{{ block.text }}
{%- else -%}
{{- raise_exception("Invalid assistant block type: " + block.type) -}}
{%- endif -%}
{%- endfor -%}
{%- else -%}
{{- raise_exception("Invalid assistant content") -}}
{%- endif -%}
{%- else -%}
{{- raise_exception("Invalid assistant message") -}}
{%- endif -%}
{%- if "tool_calls" in message and message.tool_calls -%}
{{ tool_calls_token + '[' }}
{%- for tool_call in message.tool_calls -%}
{%- if tool_call.type is defined and tool_call.type != 'function' -%}
{{- raise_exception("Invalid tool call type: " + tool_call.type) -}}
{%- endif -%}
{%- if tool_call.function %}
{%- set tool_call = tool_call.function %}
{%- endif %}
{%- if tool_call.name and tool_call.arguments is defined -%}
{{- '{"' + tool_call.name + '": ' + (tool_call.arguments if tool_call.arguments is string else tool_call.arguments | tojson) + '}' }}
{%- if not loop.last -%}
{{- ", " }}
{%- endif -%}
{%- else -%}
{{- raise_exception("Invalid tool call") -}}
{%- endif -%}
{%- endfor -%}
{{ ']' + end_tool_calls_token }}
{%- set ns.waiting_for_tool_outputs = true -%}
{%- endif -%}
{%- elif message.role == 'tool' -%}
{%- if not ns.in_assistant -%}
{{- raise_exception("Tool message outside of assistant") -}}
{%- endif -%}
{%- if not ns.in_tool -%}
{{ tool_output_start_token }}
{%- set ns.in_tool = true -%}
{%- else -%}
{{ ", "}}
{%- endif -%}
{{ message.content }}
{%- set ns.waiting_for_tool_outputs = false -%}
{%- else -%}
{{- raise_exception("Invalid message role") -}}
{%- endif -%}
{%- endfor -%}
{%- if ns.in_tool -%}
{{ tool_output_end_token }}
{%- endif -%}
{%- if ns.in_assistant and not (continue_assistant_message is defined and continue_assistant_message) and not ns.waiting_for_tool_outputs -%}
{{ end_assistant_token }}
{%- endif -%}
{%- if add_generation_prompt -%}
{{ assistant_token }}
{%- endif -%}