메뉴
HN
Hacker News 39일 전

구형 제온 서버 174번 재부팅하며 찾아낸 최적 LLM 추론 옵션

IMP
7/10
핵심 요약

10년 된 구형 CPU 환경에서 LLM(Gemma 4)을 빠르게 구동하기 위해 추론 엔진의 25개 명령어 플래그(flags)를 하나씩 제거하며 성능 변화를 측정한 실험 결과입니다. 플래그들은 하드웨어나 작업량에 따라 상호작용하므로 무작정 복사해서 붙여넣기보다 실제 환경에서 테스트해야 함을 보여줍니다. 하드웨어 제약 없이 오픈소스 모델을 최적화하려는 실무자들에게 매우 유용한 가이드입니다.

번역된 본문

발행일: 2026년 6월 15일

제목: 초당 4개의 토큰을 얻기 위해 10년 된 제온(Xeon)을 174번 재시작하고 12개의 플래그를 삭제한 이야기

읽는 데 약 22분 소요

이 글은 2016년산 제온에서 Gemma 4를 구동했던 글의 후속 편입니다. 저는 어떤 플래그들이 실제로 효과가 있는지, 어떤 것이 해로운지, 그리고 어떤 것이 그저 함정인지 찾아내기 위해 25개의 플래그로 구성된 기존 설정을 하나씩 분해해 보았습니다. 제가 튜닝한 대부분의 설정은 일반 사용자에게는 아무런 도움이 되지 않을 것입니다.

몇 주 전, 저는 GPU 없이 128GB DDR3 메모리가 탑재된 2016년형 제온(Xeon)에서 260억 개의 매개변수를 가진 모델인 Gemma 4를 읽는 속도로 구동하는 방법을 공유했습니다. 그 게시물은 해커뉴스(Hacker News) 첫 페이지에 약 8시간 동안 게재되었는데, 이는 많은 사람들이 블로그에서 복사한 25개의 플래그가 포함된 명령어를 자신의 터미널 어딘가에 그대로 붙여넣고 있으며, 정작 그 플래그들이 각각 어떤 역할을 하는지 전혀 모르고 있다는 것을 의미합니다. 해당 명령어에 관해 안 좋은 소식이 있습니다. 여러분은 아마도 그 명령어를 잘못된 방식으로 사용하고 있을 것입니다.

제가 지난 게시물에서 애석하게 전했듯, 이 플래그들의 절반은 단순히 명령어에 적어둔다고 해서 작동하지 않습니다. 일부는 올바른 하드웨어가 필요하고, 일부는 올바른 호스트 설정이 필요하며, 또 일부는 특정 작업량(workload)에서만 도움이 됩니다. 엔진은 이 모든 플래그를 오류 없이 받아들이지만, 정작 어떤 플래그가 실제로 적용되었는지에 대해서는 거의 아무런 정보도 알려주지 않습니다. 그래서 이 글에서는 여러분의 환경에서 실제로 어떤 것이 작동할지 다시 추적해 보았습니다.

이를 확인하는 방법은 절제(ablation) 테스트입니다. 작동하는 설정에서 정확히 하나의 플래그를 끄고, 어떤 변화가 있는지 측정한 다음, 다시 켜놓고 모든 플래그에 대해 차례로 동일한 작업을 반복하는 것입니다. '절제'라는 단어는 신경과학을 거쳐 머신러닝으로 차용된 용어로, 일반적으로 모델 자체의 일부(어텐션 헤드나 레이어 등)를 제거하여 그 기능이 무엇인지 알아보는 것을 의미합니다. 여기서는 모델 내부가 아닌 추론 플래그에 사용한 약간 부적절한 표현이지만, 아이디어 자체는 동일합니다. 하나를 끄고, 측정하고, 반복하여 그 차이를 통해 각 부분의 가치를 알아내는 것입니다.

이 과정은 엄청난 노동이었습니다. 정말 엄청나게요. 174번의 실행을 했으며, 매번 서버를 재시작하면서 토큰을 단 하나 생성하기도 전에 25GB에 달하는 가중치(weights)를 회전하는 디스크(스핀들 디스크)에서 다시 불러와야 했습니다. 실행 당 3개의 테스트 프롬프트를 여러 번 반복했으며, 나중에 언급할 교착 상태(deadlock) 때문에 밤샘 작업 한 번의 결과를 통째로 버려야 했습니다. 제가 이 횟수를 강조하는 이유는, 횟수 자체가 핵심이기 때문입니다. 아무도 이 플래그들 중 무엇이 중요한지 모르는 이유는, 알아내는 과정이 너무 느리고 지루해서 아무도 이런 테스트를 하지 않기 때문입니다. 제가 해냈기 때문에 이 글에서 그 정답을 공유합니다.

테스트 설정

장비는 제온 포스트에서 사용한 것과 동일합니다. 제온 E5-2620 v4, 8개의 물리적 코어, 16개의 스레드, 128GB DDR3 메모리, GPU 및 스왑(swap) 없음. 엔진은 ik_llama.cpp의 feat/gemma-4-mtp 브랜치를 사용했습니다. 검증기(verifier)는 Q8_0 포맷의 gemma-4-26B-A4B-it이며, 이와 짝을 이루는 Q8_0 포맷의 MTP 드래프터(drafter)를 사용했습니다.

저는 3가지 테스트 프롬프트를 사용할 것입니다:

  1. 간단한 채팅
  2. 요약할 약 5,000 토큰 분량의 문서
  3. 코드 생성 요청

탐욕적 디코딩(greedy decoding), 고정된 시드(seed), 새 토큰 256개, 3번의 반복 측정을 진행했으며 중간값을 보고합니다.

수치를 살펴보기 전에 짚고 넘어갈 것이 두 가지 있습니다. 먼저, 저는 원래 명령어의 llama-cli가 아닌 llama-server 환경에서 벤치마크를 실행했습니다. 서버에는 요청별 '투기적 디코딩(speculative-decoding)' 원격 측정(telemetry) 데이터가 기록되는데, 이는 주어진 요청에서 드래프터가 실제로 작동했는지 확인할 수 있는 유일하고 깔끔한 방법이며, 상위 업스트림 풀 리퀘스트(pull request)가 자체적인 벤치마크를 도출하는 방식이기도 합니다. 설정 자체는 변하지 않았고 그저 테스트를 둘러싼 뼈대(harness)만 바뀌었을 뿐입니다.

또한 아래의 모든 수치는 전체 설정에서 딱 하나의 플래그만 변경한 상태에서 측정한 것입니다. 즉, 각각의 변화량(델타)은 다른 모든 설정은 켜져 있다고 가정할 때 해당 플래그가 기여한 값이지, 진공 상태에서의 효과가 아닙니다. 플래그들은 서로 상호작용을 합니다. 투기적 디코딩(Speculation)은 스레드 수가 얼마나 중요한지를 변경하고, 리패킹(Repacking)은 플래시 어텐션(flash attention)이 읽어야 할 내용을 바꿉니다. 이러한 변화량들을 단순히 더해서 현재 설정과 초보적인 설정 사이의 격차를 설명할 수는 없으며, 저 또한 그런 척하지 않을 것입니다.

전체 현황판

아래는 제가 하나씩 설명하기 전에, 성능에 영향을 미치는 모든 레버(조절 도구)를 한곳에 모아놓은 것입니다. 디코딩 속도, 초당 토큰 수, 반복 측정의 중간값이며, 각 행은 전체 설정에서 정확히 하나만 변경했을 때의 결과입니다. 백분율은 기존에 공개된 설정 대비 수치이므로, 도움이 되는 플래그를 제거하면 수치 하락(loss)으로 나타납니다.

변경된 레버: chat lon...

원문 보기
원문 보기 (영어)
Published on June 15, 2026 I restarted a 10 year old Xeon 174 times to delete twelve flags and gain four tokens a second 22 minutes read A follow-up to running Gemma 4 on a 2016 Xeon. I took that 25-flag config apart one flag at a time, to find which ones actually do the work, which are harmful, and which are just pitfalls. Most of my tuned config will do nothing for the typical user. A couple of weeks ago I got Gemma 4, a 26-billion-parameter model, running at reading speed on a 2016 Xeon with no GPU and 128 GB of DDR3 . That post spent about eight hours on the front page of Hacker News, which means a lot of people now have a 25-flag command sitting in a terminal somewhere, copied from a blog, with no real idea which of those flags is doing the work. I have some bad news about that command. You’re likely holding it wrong. What I said in that post in passing, is that half of those flags would not take just by being present. Some need the right hardware. Some need the right host setup. Some only help on the right workload. The engine accepts all of them and tells you almost nothing about which ones actually fired. So this post is me going back and finding out what will actually work for YOU. The way you find out is an ablation. You take the working config, switch off exactly one flag, measure what changes, put it back, and do the same for every flag in turn. The word is borrowed from neuroscience by way of machine learning, where it normally means knocking out a piece of the model itself, an attention head or a layer, to see what it was for. I am using it a little improperly here, for inference flags rather than model internals, but the idea carries over: turn one thing off, measure, repeat, and the differences tell you what each piece was worth. It was a lot of work. Like a lot. One hundred and seventy-four runs, each one a fresh server reloading twenty-five gigabytes of weights off a spinning disk before it can answer a single token. Three prompts per launch, several repetitions each, and one entire overnight run I had to throw in the bin over a deadlock I will get to. I am telling you the count because the count is the point. The reason nobody knows which of these flags matter is that finding out is slow and tedious, so almost nobody does it. I did, so here is the answer. The setup The box is the same one from the Xeon post. A Xeon E5-2620 v4, eight physical cores, sixteen threads, 128 GB of DDR3, no GPU, no swap. The engine is ik_llama.cpp on the feat/gemma-4-mtp branch. The verifier is gemma-4-26B-A4B-it at Q8_0, paired with its MTP drafter at Q8_0. I’ll use 3 test prompts: a short chat turn a roughly five-thousand-token document to summarize a code generation request. Greedy decoding, fixed seed, 256 new tokens, three repetitions, median reported. Two things to clear out of the way before the numbers. I ran the benchmark under llama-server rather than the llama-cli from the original command. The server is where the per-request speculative-decoding telemetry lives, which is the only clean way to check whether the drafter actually fired on a given request, and it is what the upstream pull request drives its own benchmarks through. Nothing about the config changes, just the harness around it. And every number below is the full config with one flag changed, so each delta is that flag’s contribution given everything else is still on, not its effect in a vacuum. Flags interact. Speculation changes how much the thread count matters. Repacking changes what flash attention has to read. The deltas do not add up to the gap between this config and a naive one, and I will not pretend they do. The whole board Here is every lever that moves the needle, in one place, before I walk through them. Decode speed, tokens per second, median of the repetitions, each row the full config with exactly one thing changed. The percentage is against the published config, so a flag that helps shows up as a loss when you remove it. lever changed chat long doc code published config (autotune drafter) 12.3 6.8 15.6 drafter, fixed draft 1 15.6 (+27%) 9.8 (+44%) 15.9 (+2%) drafter, fixed draft 2 16.1 (+31%) 7.4 (+8%) 17.8 (+14%) drafter, fixed draft 3, autotune off 13.9 (+13%) 7.1 (+4%) 18.3 (+17%) drafter off entirely 12.2 (wash) 10.5 (+54%) 12.2 (−22%) flash attention off 6.7 (−46%) 4.3 (−37%) 7.5 (−52%) threads -t 4 7.9 (−35%) 5.0 (−28%) 9.5 (−39%) threads -t 16 10.8 (−12%) 7.1 (+3%) 16.2 (+4%) run-time-repack off 11.8 (−4%) 9.2 (+35%) 13.7 (−12%) Everything else I tried, --mla-use , --cpu-moe , --merge-up-gate-experts , --no-kv-offload , the -sm graph cluster, lands within a few percent of the published config on chat and code, which is to say inside the noise. The long-document column is messier than the other two and I will come back to why in the repack section. Read the negatives as the interesting part. Most of these levers do nothing or cost you, and the work is concentrated in flash attention, the physical-core thread count, and the drafter once it is configured the right way. The drafter is workload-dependent, and one setting was wrong Speculative decoding is the most tuned part of the config, and it is where I found the most. The first finding is that the drafter is not a free win across the board. Measured against turning it off entirely: On chat it is roughly a wash at the default setting. On code it is worth about twenty-eight percent . A real win. On the long document it makes things slower. Turning the drafter off is fifty-four percent faster. Summarization is hard to predict, the drafter’s acceptance rate sits down around 0.15. A draft that gets rejected most of the time is mostly overhead. So the drafter wants to be gated by workload. On for code, off for long-context work. The config has it on globally, which quietly costs you about a third of your throughput every time you summarize something. This was not something I had clocked when I wrote the original post, and it is the single most useful thing in this one. The same flag being a win, a wash, and a loss depending on the prompt is worth sitting with, because it is a tiny hand-measured example of a routing gap. The model already routes internally: a mixture-of-experts gate sends every token to the experts it judges most useful, learned and conditional, per token. But that gate optimizes for the next token, not for which inference machinery pays off on a given kind of work. The choice I am making by hand here, speculate on code, do not speculate on the summary, is a second and coarser routing decision sitting one level up, per request instead of per token, and the config hardcodes one answer to it and applies that everywhere. You could imagine a cheap classifier in front of the engine that picks the decoding policy the way the expert gate picks experts, and --spec-autotune is a clumsy first stab at that idea, aimed at a single knob and, on this workload, aimed badly. I am not saying the expert routing is broken. I am saying there is a routing decision above it that my numbers say is worth making, and I made this one with a shell script and a stopwatch… metaphorically. The stopwatch is a metaphor to be clear. The second finding is a setting I had wrong. In the Xeon post I described --spec-autotune as the way to squeeze the most speed out of speculation. Over these generations it is the worst speculation setting I tested. Every fixed --draft-max beats it. chat: autotune 12.3 tokens per second, a fixed draft of 2 gets 16.1. Twenty-five flags, and the biggest single win in the file was deleting one of them and typing a number. code: autotune 15.6, a fixed draft of 3 with autotune off gets 18.3. long document: autotune 6.9, a fixed draft of 1 gets 9.8. The mechanism is acceptance rate. A shorter, fixed draft gets accepted by the verifier more often. A draft of 1 on chat sits at 0.74 acceptance against autotune’s 0.37, and acceptance is the entire currency speculation trades in. The autotuner spends the generatio