Skip to content

Commit 253bb3f

Browse files
authored
Merge pull request #5 from nyo16/feat/qwen35-support
Add Qwen 3.5 support with reasoning_content parsing (v0.6.0)
2 parents 6d4d75a + 47bc034 commit 253bb3f

11 files changed

Lines changed: 527 additions & 24 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ llama_cpp_ex-*.tar
2626
/priv/*.so
2727
/priv/*.dylib
2828
/priv/*.dll
29+
models/

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# Changelog
22

3+
## v0.6.0
4+
5+
### Added
6+
7+
- **Qwen 3.5 support** — llama.cpp updated to c5a778891 (35 commits since v0.5.0).
8+
- **`reasoning_content` in ChatCompletion**`chat_completion/3` now splits `<think>...</think>` blocks from the response when `enable_thinking: true`. The choice message includes `reasoning_content` (the thinking text) and `content` (the final answer). Returns `nil` when thinking is not enabled or no thinking block is present.
9+
- **`reasoning_content` in ChatCompletionChunk**`stream_chat_completion/3` emits chunks with `reasoning_content` in the delta while the model is thinking, then switches to `content` after `</think>`.
10+
- **`LlamaCppEx.Thinking`** — New module with `parse/1` for one-shot parsing and `stream_parser/1` + `feed/2` for streaming token-boundary-safe parsing of think blocks. Handles the real-world Qwen3/3.5 template behavior where `<think>` is opened by the template itself.
11+
12+
### Changed
13+
14+
- **llama.cpp submodule** — Updated from 7f5ee54 to c5a778891.
15+
- ggml: add GATED_DELTA_NET op for Qwen 3.5 hybrid architecture
16+
- model: update Qwen 3.5 model type detection
17+
- convert: register Qwen 3.5 ForCausalLM for text only
18+
- CUDA: use shared mem for ssm_conv, improve performance via fewer synchronizations
19+
- Hexagon: add f32 ssm_conv, fp16 binary ops, Flash Attention optimizations
20+
- OpenCL: add l2_norm, neg, exp, diag ops
21+
- CPU: skip redundant ROPE cache updates, fix data race for debug asserts
22+
- quants: add memsets and other fixes for IQ quants
23+
- kv-cache: fix M-RoPE checkpoints, checkpoint every n tokens
24+
- server: preserve Anthropic thinking blocks in conversion
25+
26+
### Unchanged
27+
28+
- `chat/3` and `stream_chat/3` continue returning raw text (no breaking change).
29+
330
## v0.5.0
431

532
### Added

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Add `llama_cpp_ex` to your list of dependencies in `mix.exs`:
2828
```elixir
2929
def deps do
3030
[
31-
{:llama_cpp_ex, "~> 0.5.0"}
31+
{:llama_cpp_ex, "~> 0.6.0"}
3232
]
3333
end
3434
```
@@ -220,17 +220,28 @@ Multiple callers are batched into a single forward pass per tick, improving thro
220220

221221
## Benchmarks
222222

223-
Measured on Apple M4 Max (64 GB) with Qwen3-4B Q4_K_M, Metal backend (`n_gpu_layers: -1`).
223+
Measured on Apple M4 Max (64 GB), Metal backend (`n_gpu_layers: -1`).
224224

225-
### Single-sequence generation
225+
### Single-model generation speed
226+
227+
| Model | Quantization | Tokens/sec |
228+
|-------|-------------|------------|
229+
| Llama 3.2 3B Instruct | Q4_K_XL | 125.6 |
230+
| Ministral 3 3B Reasoning | Q4_K_XL | 113.0 |
231+
| Ministral 3 3B Instruct | Q4_K_XL | 104.3 |
232+
| GPT-OSS 20B | Q4_K_XL | 79.4 |
233+
| Qwen3.5-35B-A3B | Q6_K | 56.0 |
234+
| Qwen3.5-27B | Q4_K_XL | 17.5 |
235+
236+
### Single-sequence generation (Qwen3-4B Q4_K_M)
226237

227238
| Prompt | 32 tokens | 128 tokens |
228239
|--------|-----------|------------|
229240
| short (6 tok) | 0.31s (3.19 ips) | 1.01s (0.98 ips) |
230241
| medium (100 tok) | 0.36s (2.79 ips) | 1.06s (0.94 ips) |
231242
| long (500 tok) | 0.65s (1.53 ips) | 1.29s (0.77 ips) |
232243

233-
### Continuous batching throughput
244+
### Continuous batching throughput (Qwen3-4B Q4_K_M)
234245

235246
```
236247
max_tokens: 32, prompt: "short"

checksum.exs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
%{
2-
"llama_cpp_ex-nif-2.17-aarch64-apple-darwin-0.5.0.tar.gz" => "sha256:c38dac814cb760820d45101a4910ce859105267ba09a1f3e85ff7ea5c045b7bc",
3-
"llama_cpp_ex-nif-2.17-x86_64-linux-gnu-0.5.0.tar.gz" => "sha256:98f22f5d2e1de1307eb27df611b7ae5d7c436c44359cad816ad2ee36300a516c",
4-
}
1+
%{}

lib/llama_cpp_ex.ex

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ defmodule LlamaCppEx do
3636
Embedding,
3737
Grammar,
3838
ChatCompletion,
39-
ChatCompletionChunk
39+
ChatCompletionChunk,
40+
Thinking
4041
}
4142

4243
@doc """
@@ -327,6 +328,17 @@ defmodule LlamaCppEx do
327328

328329
{texts, finish_reason, completion_tokens} = collect_completion_tokens(ref, timeout)
329330

331+
raw_text = Enum.join(texts)
332+
enable_thinking = Keyword.get(chat_opts, :enable_thinking, false)
333+
334+
{reasoning_content, content} =
335+
if enable_thinking do
336+
{rc, c} = Thinking.parse(raw_text)
337+
{if(rc == "", do: nil, else: rc), c}
338+
else
339+
{nil, raw_text}
340+
end
341+
330342
completion = %ChatCompletion{
331343
id: "chatcmpl-" <> random_hex(12),
332344
object: "chat.completion",
@@ -335,7 +347,11 @@ defmodule LlamaCppEx do
335347
choices: [
336348
%{
337349
index: 0,
338-
message: %{role: "assistant", content: Enum.join(texts)},
350+
message: %{
351+
role: "assistant",
352+
content: content,
353+
reasoning_content: reasoning_content
354+
},
339355
finish_reason: finish_reason
340356
}
341357
],
@@ -427,14 +443,19 @@ defmodule LlamaCppEx do
427443
)
428444
end)
429445

446+
enable_thinking = Keyword.get(chat_opts, :enable_thinking, false)
447+
430448
%{
431449
ref: ref,
432450
gen_pid: gen_pid,
433451
timeout: timeout,
434452
id: id,
435453
created: created,
436454
model: model_name,
437-
phase: :first
455+
phase: :first,
456+
enable_thinking: enable_thinking,
457+
thinking_parser:
458+
if(enable_thinking, do: Thinking.stream_parser(thinking: true), else: nil)
438459
}
439460
end,
440461
fn
@@ -452,15 +473,45 @@ defmodule LlamaCppEx do
452473
%{phase: :streaming, ref: ref, timeout: timeout} = state ->
453474
receive do
454475
{^ref, {:token, _id, text}} ->
455-
chunk = %ChatCompletionChunk{
456-
id: state.id,
457-
object: "chat.completion.chunk",
458-
created: state.created,
459-
model: state.model,
460-
choices: [%{index: 0, delta: %{content: text}, finish_reason: nil}]
461-
}
462-
463-
{[chunk], state}
476+
if state.enable_thinking do
477+
{events, new_parser} = Thinking.feed(state.thinking_parser, text)
478+
state = %{state | thinking_parser: new_parser}
479+
480+
chunks =
481+
Enum.map(events, fn
482+
{:thinking, t} ->
483+
%ChatCompletionChunk{
484+
id: state.id,
485+
object: "chat.completion.chunk",
486+
created: state.created,
487+
model: state.model,
488+
choices: [
489+
%{index: 0, delta: %{reasoning_content: t}, finish_reason: nil}
490+
]
491+
}
492+
493+
{:content, t} ->
494+
%ChatCompletionChunk{
495+
id: state.id,
496+
object: "chat.completion.chunk",
497+
created: state.created,
498+
model: state.model,
499+
choices: [%{index: 0, delta: %{content: t}, finish_reason: nil}]
500+
}
501+
end)
502+
503+
{chunks, state}
504+
else
505+
chunk = %ChatCompletionChunk{
506+
id: state.id,
507+
object: "chat.completion.chunk",
508+
created: state.created,
509+
model: state.model,
510+
choices: [%{index: 0, delta: %{content: text}, finish_reason: nil}]
511+
}
512+
513+
{[chunk], state}
514+
end
464515

465516
{^ref, :eog} ->
466517
final_chunk = %ChatCompletionChunk{

lib/llama_cpp_ex/chat_completion.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ defmodule LlamaCppEx.ChatCompletion do
1616

1717
@type choice :: %{
1818
index: integer(),
19-
message: %{role: String.t(), content: String.t()},
19+
message: %{
20+
role: String.t(),
21+
content: String.t(),
22+
reasoning_content: String.t() | nil
23+
},
2024
finish_reason: String.t()
2125
}
2226

lib/llama_cpp_ex/chat_completion_chunk.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ defmodule LlamaCppEx.ChatCompletionChunk do
1515

1616
@type chunk_choice :: %{
1717
index: integer(),
18-
delta: %{optional(:role) => String.t(), optional(:content) => String.t()},
18+
delta: %{
19+
optional(:role) => String.t(),
20+
optional(:content) => String.t(),
21+
optional(:reasoning_content) => String.t()
22+
},
1923
finish_reason: String.t() | nil
2024
}
2125

0 commit comments

Comments
 (0)