Skip to content

log, server: self contained colors, split child commands from logs in router mode - #29895

Merged
ngxson merged 5 commits into
ggml-org:masterfrom
ServeurpersoCom:log-self-contained-colors
Oct 4, 2026
Merged

ngxson merged 5 commits into
ggml-org:masterfrom
ServeurpersoCom:log-self-contained-colors

Conversation

@ServeurpersoCom

@ServeurpersoCom ServeurpersoCom commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Overview

This fixes two problems and the technical debt behind them. Since #28747, router mode prints an empty log line on every loading and download progress update of a child, and console colors behave differently depending on the platform.

The logger now writes the color reset before the trailing newline, so every line carries its own colors. The router passes its color setting to its children, whose output ends up in its terminal, and the logger enables virtual terminal mode on the Windows console, as llama-cli already does.

The child no longer sends its state commands on the same pipe as its logs, which resolves the TODO at the spawn. It keeps stdout for the commands and points everything else written to stdout at stderr before anything is written, so no log line, library print or progress output can end up in front of a command, and the command goes back to its plain framing with no empty lines. The router reads both pipes, handles the commands from stdout, forwards stderr as the log, and warns about any other line on the command pipe.

Colors now work the same way everywhere, for every tool that logs through common: Windows 10 and 11, cmd and PowerShell, macOS and Linux, with no empty lines and no workaround.

Additional information

Reproduced with an unterminated write on stdout and stderr right before download_finished: without the pipe split the command is logged instead of handled and the model stays stuck downloading, with it every command is handled and the stray bytes land in the log. Also checked model load and unload, download, --log-jsonl, colored logs and the router tests. Thanks @eapache for spotting the log calls that don't end with a newline.

Follow-up #28747
Fixes #29878

Requirements

The logger writes the color reset after the trailing newline, so the
reset opens the next line. On the shared pipe of a router child it lands
in front of the next state command, which the router then misses, and
the line break that works around it shows up as an empty log line on
every progress update.

The reset now goes before the trailing newlines, so every line is self
contained and the command goes back to its plain framing. The router
passes its effective color setting to its children, whose output ends
up in its terminal, and leaves that option out when comparing presets
on reload.
A Windows console renders ANSI sequences only in virtual terminal mode,
which nothing turns on for the logger, so llama-server prints raw escape
codes on the Windows 10 console while llama-cli, whose console code
enables it, shows colors. The logger now enables virtual terminal mode
on stdout and stderr when it turns colors on, and keeps colors off when
a console cannot render them. Pipes and files take the sequences as is.
@ServeurpersoCom
ServeurpersoCom requested review from a team as code owners October 3, 2026 07:56
@ServeurpersoCom

ServeurpersoCom commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Tested on Windows 10 and 11 (cmd and PowerShell), macOS and Linux, screenshots below; the empty lines are gone too, easiest to check by redirecting stdout/stderr to a file, same result on every OS.

Color bug example :

Windows 10 is the special case: its console only renders colors once virtual terminal mode is enabled, which tty_enable_ansi() now does when the logger turns colors on, while Windows 11 terminals render them out of the box.

Windows 10 No fix

Regression tests :

Windows 10 With fix Windows 11 with fix

Windows 10 old powershell 5
Windows 10 powershell with fix

Windows 11 powershell
Windows 11 powershell with fix

MacOS
MacOS with fix

Linux Debian
Debian with fix

@angt

angt commented Oct 3, 2026

Copy link
Copy Markdown
Member

I think we should keep the "\n%s%s\n" format, but just forward only non empty lines ? Feel safer and easier 🤔

@ServeurpersoCom

Copy link
Copy Markdown
Contributor Author

I think we should keep the "\n%s%s\n" format, but just forward only non empty lines ? Feel safer and easier 🤔

This PR fixes the cause: every log line now closes its own colors, so nothing written to the shared pipe is left unterminated before a command. The known partial writers are covered too, since the loading dots and the download progress bar never print in a child. Validated with the #28747 repro and its negative control, no empty lines on Linux, macOS and Windows, and the router tests passing.

Keeping "\n%s%s\n" only works together with a filter that hides the empty lines it creates, and that filter would also swallow the empty lines children log on purpose. It would add a safety net for a case that doesn't exist in the repo today, a third party writing to stderr without a trailing newline, whose worst outcome would be one lost command as before #28747.

The way to remove that risk by construction is the TODO at the spawn in server-models.cpp: separate stdout for commands from stderr for logs, so no framing is needed at all.

Would you rather keep the code clean at the source and leave the remaining risk to that TODO, or keep the framing and the filter as a guard in the meantime?

@ServeurpersoCom

Copy link
Copy Markdown
Contributor Author

What bothers me with the filter is that the router stops forwarding the child output as is: the same log reads differently standalone and behind the router. That said, if you really want that extra safety net, I'll add the leading newline and the filter back to this PR.

@angt

angt commented Oct 3, 2026

Copy link
Copy Markdown
Member

The current approach is very fragile, so I guess there will have regression every time we touch that code.
Was just a warning... we will iterate fixing that, I guess 😅

@ServeurpersoCom

Copy link
Copy Markdown
Contributor Author

The current approach is very fragile, so I guess there will have regression every time we touch that code. Was just a warning... we will iterate fixing that, I guess 😅

The TODO at the spawn is resolved in ServeurpersoCom@58627fc, tested successfully on Linux, macOS, Windows 10 and Windows 11, so the fragility is gone: commands and logs no longer share a pipe. It builds on this PR, would you rather have it pushed here?

@eapache

eapache commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

This seems to fix the blank-line issue I reported. However, I asked my Astra for a review and it pointed out that not all log output ends with a newline today (e.g. the warning at common/chat.cpp:1322).

If a state notification follows before another newline, the output becomes ...warning.cmd_child_to_router:state:.... The router only recognizes commands at the beginning of a line, so it silently drops that state update.

@ServeurpersoCom

Copy link
Copy Markdown
Contributor Author

This seems to fix the blank-line issue I reported. However, I asked my Astra for a review and it pointed out that not all log output ends with a newline today (e.g. the warning at common/chat.cpp:1322).

If a state notification follows before another newline, the output becomes ...warning.cmd_child_to_router:state:.... The router only recognizes commands at the beginning of a line, so it silently drops that state update.

Thanks for testing and for the catch, you're right, a few log calls don't end with a newline. That's exactly what the follow-up fixes by construction, commands get their own pipe so nothing written to the log can end up in front of them: ServeurpersoCom@58627fc

So I think the scope of this PR should grow a bit: I'm bringing the TODO in here, so the whole topic is handled at once, the empty lines, the colors, and the related technical debt.

The child sent its state commands on the same pipe as its logs, so the
router had to pick them out of the log stream by a line prefix, and any
unterminated write in front of a command made the router miss it. This
resolves the TODO at the spawn that called for splitting stdout and
stderr.

The child now keeps stdout for the commands and points everything else
written to stdout at stderr, before anything is written. The router
reads both pipes, handles the commands from stdout and forwards stderr
as the log, and warns about any other line on the command pipe.
@ServeurpersoCom ServeurpersoCom changed the title log: self contained colors, fix empty lines in router mode log, server: self contained colors, split child commands from logs in router mode Oct 3, 2026
@ServeurpersoCom

Copy link
Copy Markdown
Contributor Author

This seems to fix the blank-line issue I reported. However, I asked my Astra for a review and it pointed out that not all log output ends with a newline today (e.g. the warning at common/chat.cpp:1322).

The pipe split is now pushed to this PR, could you give it another try in your setup when you have a moment? Thanks again for the review!

@eapache

eapache commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

This now fixes the code review issue and mostly fixes the blank line issue. Interestingly as of the latest commit I now get a single blank log line when loading a model in router mode, right between load_hparams and load_model e.g.

0.00.000.764 W server tools or MCP servers are enabled, using localhost as default CORS origin (change via --cors-origins)
0.00.000.816 I srv  llama_server: initializing ...
0.00.244.707 I cmn  common_param: common_params_print_info: verbosity = 3 (adjust with the `-lv N` CLI arg)
0.00.247.808 I srv    operator(): Available models (4):
0.00.247.810 I srv    operator():   [    preset] Gemma4-26B-A4B
0.00.247.810 I srv    operator():   [    preset] Impish-Bloodmoon
0.00.247.810 I srv    operator():   [    preset] Qwen3.8-27B
0.00.247.810 I srv    operator():   [    preset] Qwen3.8-Flash-Next
0.00.247.892 W srv  llama_server: security: router mode, MCP proxy (experimental), server tools (experimental) enabled - do not expose to untrusted environments
0.00.247.894 I srv  llama_server: starting server in router mode. models will be automatically loaded on-demand
0.00.250.176 I srv  llama_server: listening on http://0.0.0.0:9931
0.05.399.672 I srv          load: spawning server instance with name=Qwen3.8-27B on port 33841
0.05.399.705 I srv          load: spawning server instance with args:
0.05.399.706 I srv          load:   /home/eapache/src/llama.cpp/build/bin/llama-server
0.05.399.706 I srv          load:   --host
0.05.399.706 I srv          load:   127.0.0.1
0.05.399.706 I srv          load:   --log-colors
0.05.399.707 I srv          load:   on
0.05.399.707 I srv          load:   --min-p
0.05.399.707 I srv          load:   0.0
0.05.399.707 I srv          load:   --port
0.05.399.707 I srv          load:   33841
0.05.399.708 I srv          load:   --presence-penalty
0.05.399.708 I srv          load:   0.0
0.05.399.708 I srv          load:   --reasoning-preserve
0.05.399.708 I srv          load:   --repeat-penalty
0.05.399.708 I srv          load:   1.0
0.05.399.708 I srv          load:   --spec-draft-n-max
0.05.399.709 I srv          load:   2
0.05.399.709 I srv          load:   --spec-type
0.05.399.709 I srv          load:   draft-mtp
0.05.399.709 I srv          load:   --temperature
0.05.399.709 I srv          load:   1.0
0.05.399.710 I srv          load:   --tools
0.05.399.710 I srv          load:   get_info
0.05.399.710 I srv          load:   --top-k
0.05.399.710 I srv          load:   20
0.05.399.710 I srv          load:   --top-p
0.05.399.710 I srv          load:   0.95
0.05.399.711 I srv          load:   --webui-mcp-proxy
0.05.399.711 I srv          load:   --alias
0.05.399.711 I srv          load:   Qwen3.8-27B
0.05.399.711 I srv          load:   --fit-target
0.05.399.711 I srv          load:   256,512
0.05.399.711 I srv          load:   --model
0.05.399.712 I srv          load:   ./models/custom/Qwen3.8-27B/Qwen3.8-27B-UD-Q5_K_XL.gguf
0.05.399.712 I srv          load:   --mmproj
0.05.399.712 I srv          load:   ./models/custom/Qwen3.8-27B/mmproj-BF16.gguf
0.05.399.712 I srv          load:   --mmproj-device
0.05.399.712 I srv          load:   CUDA1
0.05.399.713 I srv          load:   --tensor-split
0.05.399.713 I srv          load:   6,1
[33841] 0.00.049.153 W server tools or MCP servers are enabled, using localhost as default CORS origin (change via --cors-origins)
[33841] 0.00.049.239 I srv  llama_server: initializing ...
[33841] 0.00.049.259 I cmn  common_param: common_params_print_info: verbosity = 3 (adjust with the `-lv N` CLI arg)
[33841] 0.00.049.562 W srv  llama_server: security: MCP proxy (experimental), server tools (experimental) enabled - do not expose to untrusted environments
[33841] 0.00.052.054 I srv    load_model: loading model './models/custom/Qwen3.8-27B/Qwen3.8-27B-UD-Q5_K_XL.gguf'
[33841] 0.01.288.257 W common_fit_params: failed to fit params to free device memory: model_params::tensor_split already set by user, abort
[33841] 0.10.599.649 I cmn          init: llama threadpool init, n_threads = 12
[33841] 0.10.694.571 I common_speculative_init_result: creating MTP draft context against the target model './models/custom/Qwen3.8-27B/Qwen3.8-27B-UD-Q5_K_XL.gguf'
[33841] 0.10.844.422 W load_hparams: Qwen-VL models require at minimum 1024 image tokens to function correctly on grounding tasks
[33841] 0.10.844.423 W load_hparams: if you encounter problems with accuracy, try adding --image-min-tokens 1024
[33841] 0.10.844.424 W load_hparams: more info: https://github.com/ggml-org/llama.cpp/issues/16842
[33841] 
[33841] 0.11.515.001 I srv    load_model: loaded multimodal model, './models/custom/Qwen3.8-27B/mmproj-BF16.gguf'
[33841] 0.11.515.026 I srv    load_model: initializing, n_slots = 4, n_ctx_slot = 221184, kv_unified = 'true'
[33841] 0.11.723.583 I srv  llama_server: model loaded
[33841] 0.11.723.589 I srv  llama_server: listening on http://127.0.0.1:33841
0.17.164.532 I srv  proxy_reques: proxying request to model Qwen3.8-27B on port 33841

@eapache

eapache commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Astra tracked this down to tools/mtmd/clip.cpp:1684 which has a log with an explicit double-\n\n. So I think it's outside of this PR though we could probably just drop the excess \n.

@ServeurpersoCom

Copy link
Copy Markdown
Contributor Author

Astra tracked this down to tools/mtmd/clip.cpp:1684 which has a log with an explicit double-\n\n. So I think it's outside of this PR though we could probably just drop the excess \n.

Yes, that one is intended: the router now forwards the child output exactly as is, so this empty line comes from clip.cpp itself and shows up the same way in standalone mode. Whether to keep that kind of spacing or rework those logs is a separate choice, and it's now entirely up to each log call, the router stays out of it.

@angt

angt commented Oct 3, 2026

Copy link
Copy Markdown
Member

Way better like this! Thanks @ServeurpersoCom

@ServeurpersoCom
ServeurpersoCom requested a review from angt October 3, 2026 14:01
Comment thread tools/server/server-models.h Outdated
Comment on lines +324 to +328
static bool is_child();

// keep stdout for the commands to the router, called before anything else is written;
// everything else written to stdout goes to stderr with the logs
static void init();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite comfortable spreading the static across the code base. It's just not an elegant solution, making it unclear about the ownership of the object

Instead, should avoid static whenever possible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, the command stream is now a member of the single server_child instance, no static anymore.

Comment thread tools/server/server.cpp Outdated
Comment on lines +93 to +96
if (server_child::is_child()) {
server_child::init();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an instance of server_child will be created later in this function anyway, why need to make it singleton here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the instance is now created first in the entry point and passed down, so there is only one.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having both setup() and init() is confusing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init() is gone, the constructor does it, so only setup() remains.

The single server_child is now created first in the entry point and its
constructor keeps stdout for the commands, so the stream is a member of
the instance instead of a static, and init() is gone. The instance is
passed down to the server, while the CLI entry point creates its own.
@ServeurpersoCom
ServeurpersoCom requested a review from ngxson October 4, 2026 14:56
Comment thread tools/server/server.cpp Outdated
@ngxson
ngxson merged commit a7fb71f into ggml-org:master Oct 4, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Misc. bug: blank log lines in router mode

4 participants