실제 터미널 캡처: ollama show로 본 Q4_K_M 양자화 정보, ollama run --verbose 처리 속도, ollama ps의 595 MB·60%/40% CPU/GPU 표시와 AMX 버퍼 로그 줄, 그리고 글 제목

LLM 메모리 계산: CPU에서 잰 Q4_K_M 6.16비트와 Ollama 적재 크기

양자화 이름의 앞자리와 /api/ps의 size는 VRAM 요구량으로 그대로 옮기기 쉬운 숫자입니다. 여기서는 그 숫자들을 가중치·KV 계산과 런타임 관측으로 나눠 읽는 것이 테스트 목적입니다. 작은 Qwen 모델을 운영 서버에 올릴 용량을 산정하려면 모델 파일이 들어가는지뿐 아니라 요청 설정을 바꿨을 때 메모리가 얼마나 늘어나는지도 알아야 해서, Debian 13에서 Q4_K_M GGUF의 실제 텐서 용량과 컨텍스트에 따른 Ollama 적재 크기를 비교했습니다. 이번 대상은 로컬 모델 qwen2.5-0.5b-local입니다. GPU가 없는 서버인데도 ollama ps가 CPU/GPU 비율을 보여 주는데, 그 비율이 무엇을 세는지는 뒤에서 서버 로그로 따로 밝힙니다.

이름 앞자리만 곱한 추정은 태그 크기보다 작고, 파일을 열어 잰 평균 비트는 그 앞자리와 다릅니다. 런타임 size는 텐서 바이트에 f16 KV를 더한 값과도 차이가 있으며, 컨텍스트에 따른 변화는 점검 스크립트의 출력에서 비교합니다.

해결 순서는 세 스크립트입니다. 재현 스크립트는 양자화 이름을 순수 Q숫자 형식으로 검사하다 ValueError로 끝납니다. 해결 스크립트는 GGUF 텐서와 공식 config.json을 맞추고, num_ctx 4096과 32768의 size를 f16 KV 계산과 나란히 둡니다. 점검 스크립트는 general.file_type 정수를 열거 이름으로 바꾸고, 두 size의 차이를 토큰 수로 나눠 f16 공식과 비교합니다. 코드는 각 스크립트에서 발췌했으며, 실행 출력은 코드 아래에 있습니다.

Q4_K_M 이름을 비트 폭으로 읽으면 나는 오류

재현 스크립트는 로컬 Ollama의 /api/tags에서 qwen2.5-0.5b-local 한 건을 고릅니다. quantization_level이 Q 뒤에 숫자만 있는 형식인지 fullmatch로 검사하고, 맞지 않으면 ValueError를 냅니다. 예외 앞에서는 이름에서 찾은 첫 숫자를 비트 수로 보고 parameter_size 라벨과 곱한 추정 바이트를 태그 size와 함께 찍습니다. 분모로 쓰는 라벨은 텐서 원소 합이 아니라 태그에 적힌 문자열입니다.

import json, re
data = json.load(open("tmp/tags.json"))
models = [m for m in data["models"] if str(m.get("name", "")).startswith("qwen2.5-0.5b-local")]
if len(models) != 1:
    raise SystemExit("expected one qwen2.5-0.5b-local model")
m = models[0]
q = m["details"]["quantization_level"]
label = m["details"]["parameter_size"]
tag_size = int(m["size"])
if not label.endswith("M"):
    raise SystemExit("parameter_size label is not M")
whole, frac = label[:-1].split(".")
frac = (frac + "000000")[:6]
params = int(whole) * 10**6 + int(frac)
bits = int(re.search(r"Q(\d+)", q).group(1))
est = params * bits // 8
print(f"model={m['name']}")
print(f"quantization_level={q}")
print(f"parameter_size_label={label}")
print(f"parameter_size_parsed={params}")
print(f"assumed_bits={bits}")
print(f"naive_estimate_bytes={est}")
print(f"tag_size_bytes={tag_size}")
print(f"tag_minus_naive_bytes={tag_size - est}")
if re.fullmatch(r"Q(\d+)", q) is None:
    raise ValueError(
        f"Error: quantization name {q!r} is not a plain Q<bits> width; "
        f"{bits}-bit estimate {est} bytes is below tag size {tag_size} bytes"
    )

다음 출력은 이름 검사 코드가 실제로 멈춘 지점을 보여 줍니다. 앞자리 4로 계산한 값은 315085000바이트인데 태그 크기는 491400150바이트입니다. 이 ValueError는 모델 적재 실패가 아니라, Q4_K_M을 고정 비트 폭으로 해석하지 못하도록 스크립트가 낸 예외입니다.

+ date +%F
2026-10-14
+ curl -sS -m 10 http://127.0.0.1:11434/api/tags -o tmp/tags.json
+ python3 -
model=qwen2.5-0.5b-local:latest
quantization_level=Q4_K_M
parameter_size_label=630.17M
parameter_size_parsed=630170000
assumed_bits=4
naive_estimate_bytes=315085000
tag_size_bytes=491400150
tag_minus_naive_bytes=176315150
Traceback (most recent call last):
  File "<stdin>", line 26, in <module>
ValueError: Error: quantization name 'Q4_K_M' is not a plain Q<bits> width; 4-bit estimate 315085000 bytes is below tag size 491400150 bytes
LLM 메모리 계산 실행 화면: 4비트 추정치가 tag 크기보다 작아 Q4_K_M 계산은 ValueError를 냅니다
2026-10-14 14:26:42 KST, Debian 13 — 4비트 추정치가 tag 크기보다 작아 Q4_K_M 계산은 ValueError를 냅니다.

캡처는 재현 스크립트가 태그를 읽은 뒤 ValueError로 종료한 화면입니다. 실행일은 2026-10-14이고 대상 모델은 qwen2.5-0.5b-local:latest입니다. parameter_size_label 630.17M을 풀어 얻은 630170000에 4비트를 적용했지만, 태그보다 176315150바이트 작은 추정이 나왔습니다. 이 차이 때문에 용량 계획에서는 라벨의 원소 수와 이름에서 꺼낸 비트 수를 곱하는 단계를 텐서 실측으로 바꿔야 합니다.

이어서 트레이스백의 ValueError가 납니다. 메시지는 Q4_K_M이 순수 Q비트 폭이 아니며, 4비트 추정 315085000바이트가 태그 크기 491400150바이트보다 작다는 내용입니다. 느슨한 검색은 이름 앞의 4를 집어 곱을 만들었지만, 전체 이름이 Q와 숫자만으로 이루어지지 않아 형식 검사에서 멈춘 것입니다.

Q4_K_M의 4를 모든 가중치의 비트 폭으로 본 것이 원인

이 파일에서 앞자리 4를 모든 가중치의 비트 수로 두면 태그 크기와 맞지 않습니다. 이번 태그는 그 곱보다 176315150바이트 큽니다. 다음 절의 quant_bytes가 타입별 숫자로 보여 주듯, 이 파일에서 Q4_K 비중은 6.1에 그칩니다.

이름으로 계산하는 방식은 /api/tags의 quantization_level과 parameter_size만 있으면 추정 바이트가 바로 나온다는 점이 편합니다. 하지만 Q4_K_M은 Q 뒤에 숫자만 붙는 형식이 아니어서 ValueError로 끝나고, 앞자리 4로 만든 315085000바이트는 태그 491400150바이트보다 176315150바이트나 작습니다.

텐서 바이트와 f16 KV로 size를 나누는 계산

해결 스크립트는 같은 박스에서 환경을 찍고, gguf 0.19.0 가상환경을 준비합니다. 공식 config.json과 README를 받고, num_ctx만 4096과 32768로 바꿔 generate를 두 번 호출합니다. 프롬프트는 hi이고 num_predict는 1입니다. generate와 /api/ps, ollama ps는 flock으로 잡은 한 구간에서 실행합니다. KV는 key와 value 두 장에, 헤드 차원과 KV 헤드 수, 층 수를 곱하고, f16을 가정해 원소당 2바이트를 다시 곱합니다.

set -eu
cd "$(dirname "$0")/.."
export PYTHONPYCACHEPREFIX="$PWD/run/pycache"
export PIP_CACHE_DIR="$PWD/run/pip-cache"
export TMPDIR="$PWD/run/tmp"
mkdir -p "$PYTHONPYCACHEPREFIX" "$PIP_CACHE_DIR" "$TMPDIR"
GGUF=/home/user/hands/gguf/qwen2.5-0.5b-instruct-q4_k_m.gguf

echo "+ date +%F"
date +%F
echo "+ uname -srm"
uname -srm
echo "+ grep ^PRETTY_NAME= /etc/os-release"
grep ^PRETTY_NAME= /etc/os-release
echo "+ python3 --version"
python3 --version
echo "+ ollama --version"
ollama --version
echo "+ nproc"
nproc

이 함수는 컨텍스트별 요청과 적재 상태 조회를 한데 묶어 같은 실행끼리 비교하려고 만든 것입니다. num_ctx에 4096과 32768을 명시하고, 응답 뒤의 context_length도 맞는지 검사합니다. 주석에 적은 대로 다른 요청이 러너를 갈아 끼우면 빈 배열을 읽을 수 있어서, flock 없이 얻은 스냅샷은 계산에 넣지 않습니다. 컨텍스트가 맞지 않으면 코드에 있는 재요청과 재검사로 비교 대상을 다시 맞춥니다.

gen() {
  ctx="$1"
  # 같은 모델의 다른 num_ctx 요청이 러너를 갈아끼우면 /api/ps가 빈 배열이 된다.
  # generate와 ps 스냅샷을 한 락 안에서 끝낸다.
  (
    echo "+ flock 9"
    flock 9
    echo "+ curl -sS -m 25 http://127.0.0.1:11434/api/generate num_ctx=${ctx}"
    curl -sS -m 25 http://127.0.0.1:11434/api/generate \
      -H 'Content-Type: application/json' \
      -d "{\"model\":\"qwen2.5-0.5b-local\",\"prompt\":\"hi\",\"stream\":false,\"keep_alive\":\"5m\",\"options\":{\"num_ctx\":${ctx},\"num_predict\":1}}" \
      -o "tmp/gen_${ctx}.json"
    echo "+ curl -sS -m 10 http://127.0.0.1:11434/api/ps"
    curl -sS -m 10 http://127.0.0.1:11434/api/ps -o "tmp/ps_${ctx}.json"
    echo "+ python3 -c check ps context ${ctx}"
    if ! python3 -c 'import json,sys; ctx=int(sys.argv[1]); p=sys.argv[2]; ps=json.load(open(p)); ms=[m for m in ps.get("models",[]) if str(m.get("name","")).startswith("qwen2.5-0.5b-local") and int(m.get("context_length") or 0)==ctx]; raise SystemExit(0 if ms else 1)' "$ctx" "tmp/ps_${ctx}.json"; then
      echo "+ curl -sS -m 25 http://127.0.0.1:11434/api/generate num_ctx=${ctx} retry"
      curl -sS -m 25 http://127.0.0.1:11434/api/generate \
        -H 'Content-Type: application/json' \
        -d "{\"model\":\"qwen2.5-0.5b-local\",\"prompt\":\"hi\",\"stream\":false,\"keep_alive\":\"5m\",\"options\":{\"num_ctx\":${ctx},\"num_predict\":1}}" \
        -o "tmp/gen_${ctx}.json"
      echo "+ curl -sS -m 10 http://127.0.0.1:11434/api/ps"
      curl -sS -m 10 http://127.0.0.1:11434/api/ps -o "tmp/ps_${ctx}.json"
      echo "+ python3 -c check ps context ${ctx}"
      python3 -c 'import json,sys; ctx=int(sys.argv[1]); p=sys.argv[2]; ps=json.load(open(p)); ms=[m for m in ps.get("models",[]) if str(m.get("name","")).startswith("qwen2.5-0.5b-local") and int(m.get("context_length") or 0)==ctx]; raise SystemExit(0 if ms else 1)' "$ctx" "tmp/ps_${ctx}.json"
    fi
    echo "+ ollama ps"
    ollama ps
  ) 9>"$PWD/run/tmp/ollama-ps.lock"
}

gen 4096
gen 32768

이제 파일 내부의 텐서 바이트를 합산해 이름 대신 실제 가중치 평균을 구합니다. 공식 config.json과 GGUF의 구조 값을 대조하고, head_dim은 임베딩 길이와 헤드 수로 계산합니다. f16 KV는 토큰당 계산값과 컨텍스트 길이를 곱하되, 그 합을 곧바로 Ollama의 전체 적재 크기로 단정하지 않습니다. 마지막 비교에는 각 요청에서 저장한 size와 size_vram을 별도로 사용합니다.

import json, os, gguf
from collections import Counter

gguf_path = "/home/user/hands/gguf/qwen2.5-0.5b-instruct-q4_k_m.gguf"
reader = gguf.GGUFReader(gguf_path)
arch = reader.fields["general.architecture"].contents()
layers = int(reader.fields[f"{arch}.block_count"].contents())
heads = int(reader.fields[f"{arch}.attention.head_count"].contents())
kv_heads = int(reader.fields[f"{arch}.attention.head_count_kv"].contents())
embed = int(reader.fields[f"{arch}.embedding_length"].contents())
ffn = int(reader.fields[f"{arch}.feed_forward_length"].contents())
ctx_meta = int(reader.fields[f"{arch}.context_length"].contents())
file_type = int(reader.fields["general.file_type"].contents())
head_dim = embed // heads
per_tok = 2 * layers * kv_heads * head_dim * 2

n_elem = 0
n_bytes = 0
by_type = Counter()
emb_n = None
out_n = None
for t in reader.tensors:
    ne = int(t.n_elements)
    nb = int(t.n_bytes)
    n_elem += ne
    n_bytes += nb
    by_type[t.tensor_type.name] += nb
    if t.name == "token_embd.weight":
        emb_n = ne
    if t.name == "output.weight":
        out_n = ne

file_bytes = os.path.getsize(gguf_path)
tags = json.load(open("tmp/tags.json"))
tag = [m for m in tags["models"] if str(m.get("name","")).startswith("qwen2.5-0.5b-local")][0]
tag_size = int(tag["size"])
blob = None
for line in open("tmp/modelfile.txt"):
    if line.startswith("FROM /"):
        blob = line.split()[1].strip()
blob_bytes = os.path.getsize(blob) if blob else -1

cfg = json.load(open("tmp/config.json"))
pairs = [
    ("num_hidden_layers", layers),
    ("num_attention_heads", heads),
    ("num_key_value_heads", kv_heads),
    ("hidden_size", embed),
    ("intermediate_size", ffn),
    ("max_position_embeddings", ctx_meta),
]
print(f"architecture={arch}")
print(f"layers={layers}")
print(f"heads={heads}")
print(f"kv_heads={kv_heads}")
print(f"embed={embed}")
print(f"head_dim={head_dim}")
print(f"feed_forward_length={ffn}")
print(f"context_length={ctx_meta}")
print(f"general_file_type={file_type}")
print(f"config_has_head_dim={'head_dim' in cfg}")
for key, got in pairs:
    print(f"config_match {key} config={cfg[key]} gguf={got} equal={cfg[key] == got}")

print(f"tensor_elements={n_elem}")
print(f"tensor_bytes={n_bytes}")
print(f"bits_per_weight_raw={n_bytes * 8 / n_elem:.12f}")
print(f"bits_per_weight_rounded={n_bytes * 8 / n_elem:.3f}")
print(f"file_bytes={file_bytes}")
print(f"tag_size_bytes={tag_size}")
print(f"file_minus_tensor_bytes={file_bytes - n_bytes}")
print(f"file_bytes_equal_tag={file_bytes == tag_size}")
print(f"blob_bytes={blob_bytes}")
print(f"file_bytes_equal_blob={file_bytes == blob_bytes}")
print(f"token_embd_elements={emb_n}")
print(f"output_elements={out_n}")
print(f"elements_minus_one_embedding={n_elem - emb_n}")
print(f"elements_minus_both_embeddings={n_elem - emb_n - out_n}")
for name, nb in sorted(by_type.items(), key=lambda kv: -kv[1]):
    pct = 100.0 * nb / n_bytes
    print(f"quant_bytes {name} bytes={nb} pct={pct:.1f}")

weight_mib = round(n_bytes / 2**20)
print(f"weight_mib_round={weight_mib}")
print(f"kv_f16_bytes_per_token={per_tok}")
for ctx in (4096, 32768):
    kv_bytes = per_tok * ctx
    kv_mib = round(kv_bytes / 2**20)
    ps = json.load(open(f"tmp/ps_{ctx}.json"))
    model = [x for x in ps["models"] if str(x.get("name","")).startswith("qwen2.5-0.5b-local")][0]
    gen = json.load(open(f"tmp/gen_{ctx}.json"))
    size = int(model["size"])
    vram = int(model["size_vram"])
    size_mib = round(size / 2**20)
    sum_mib = weight_mib + kv_mib
    gap = size_mib - sum_mib
    print(f"gen_done ctx={ctx} done={gen.get('done')} done_reason={gen.get('done_reason')} eval_count={gen.get('eval_count')}")
    print(f"ps_context_length ctx={ctx} value={model.get('context_length')}")
    print(f"ps_size_bytes ctx={ctx} value={size}")
    print(f"ps_size_div_1e6 ctx={ctx} value={size / 1e6:.1f}")
    print(f"ps_size_div_1e9 ctx={ctx} value={size / 1e9:.3f}")
    print(f"ps_size_vram_bytes ctx={ctx} value={vram}")
    print(f"kv_mib ctx={ctx} value={kv_mib}")
    print(f"RESULT ctx={ctx} weight_mib={weight_mib} kv_mib={kv_mib} sum_mib={sum_mib} ollama_mib={size_mib} gap_mib={gap}")

아래 실행 기록은 환경 정보부터 컨텍스트별 적재 결과까지 이어집니다. 텐서 평균은 6.163비트이며, 가중치와 f16 KV의 합에 대한 size 차이는 57MiB와 153MiB로 나옵니다. done=True와 요청한 context_length를 함께 읽어야 계산에 사용한 모델이 해당 컨텍스트로 실행됐음을 판단할 수 있습니다.

+ date +%F
2026-10-14
+ uname -srm
Linux 6.12.94+ x86_64
+ grep ^PRETTY_NAME= /etc/os-release
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
+ python3 --version
Python 3.13.5
+ ollama --version
ollama version is 0.40.3
+ nproc
8
+ free -h
               total        used        free      shared  buff/cache   available
Mem:            15Gi        12Gi       1.3Gi       169Mi       2.5Gi       3.3Gi
Swap:             0B          0B          0B
+ python3 -m venv venv
+ venv/bin/pip install --disable-pip-version-check gguf==0.19.0
Requirement already satisfied: gguf==0.19.0 in ./run/venv/lib/python3.13/site-packages (0.19.0)
Requirement already satisfied: numpy>=1.17 in ./run/venv/lib/python3.13/site-packages (from gguf==0.19.0) (2.5.4)
Requirement already satisfied: pyyaml>=5.1 in ./run/venv/lib/python3.13/site-packages (from gguf==0.19.0) (6.0.3)
Requirement already satisfied: requests>=2.25 in ./run/venv/lib/python3.13/site-packages (from gguf==0.19.0) (2.34.2)
Requirement already satisfied: tqdm>=4.27 in ./run/venv/lib/python3.13/site-packages (from gguf==0.19.0) (4.70.1)
Requirement already satisfied: charset_normalizer<4,>=2 in ./run/venv/lib/python3.13/site-packages (from requests>=2.25->gguf==0.19.0) (3.5.2)
Requirement already satisfied: idna<4,>=2.5 in ./run/venv/lib/python3.13/site-packages (from requests>=2.25->gguf==0.19.0) (3.20)
Requirement already satisfied: urllib3<3,>=1.26 in ./run/venv/lib/python3.13/site-packages (from requests>=2.25->gguf==0.19.0) (2.8.0)
Requirement already satisfied: certifi>=2023.5.7 in ./run/venv/lib/python3.13/site-packages (from requests>=2.25->gguf==0.19.0) (2026.7.22)
+ venv/bin/python -c import importlib.metadata
gguf_version=0.19.0
+ curl -sS -m 10 http://127.0.0.1:11434/api/tags -o tmp/tags.json
+ curl -fsS -m 20 -o tmp/config.json https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct/raw/main/config.json
+ curl -fsS -m 20 -o tmp/readme.md https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct/raw/main/README.md
+ ollama show qwen2.5-0.5b-local --modelfile
# Modelfile generated by "ollama show"
# To build a new Modelfile based on this, replace FROM with:
# FROM qwen2.5-0.5b-local:latest

FROM /home/user/.ollama/models/blobs/sha256-74a4da8c9fdbcd15bd1f6d01d621410d31c6fc00986f5eb687824e7b93d7a9db
TEMPLATE {{ .Prompt }}

+ flock 9
+ curl -sS -m 25 http://127.0.0.1:11434/api/generate num_ctx=4096
+ curl -sS -m 10 http://127.0.0.1:11434/api/ps
+ python3 -c check ps context 4096
+ ollama ps
NAME                         ID              SIZE      PROCESSOR          CONTEXT    RUNNER      UNTIL              
qwen2.5-0.5b-local:latest    9806541f10f4    595 MB    60%/40% CPU/GPU    4096       llamacpp    4 minutes from now    
+ flock 9
+ curl -sS -m 25 http://127.0.0.1:11434/api/generate num_ctx=32768
+ curl -sS -m 10 http://127.0.0.1:11434/api/ps
+ python3 -c check ps context 32768
+ ollama ps
NAME                         ID              SIZE      PROCESSOR          CONTEXT    RUNNER      UNTIL              
qwen2.5-0.5b-local:latest    9806541f10f4    1.0 GB    77%/23% CPU/GPU    32768      llamacpp    4 minutes from now    
+ venv/bin/python -
architecture=qwen2
layers=24
heads=14
kv_heads=2
embed=896
head_dim=64
feed_forward_length=4864
context_length=32768
general_file_type=15
config_has_head_dim=False
config_match num_hidden_layers config=24 gguf=24 equal=True
config_match num_attention_heads config=14 gguf=14 equal=True
config_match num_key_value_heads config=2 gguf=2 equal=True
config_match hidden_size config=896 gguf=896 equal=True
config_match intermediate_size config=4864 gguf=4864 equal=True
config_match max_position_embeddings config=32768 gguf=32768 equal=True
tensor_elements=630167424
tensor_bytes=485452288
bits_per_weight_raw=6.162835710149
bits_per_weight_rounded=6.163
file_bytes=491400032
tag_size_bytes=491400150
file_minus_tensor_bytes=5947744
file_bytes_equal_tag=False
blob_bytes=491400032
file_bytes_equal_blob=True
token_embd_elements=136134656
output_elements=136134656
elements_minus_one_embedding=494032768
elements_minus_both_embeddings=357898112
quant_bytes Q5_0 bytes=266742784 pct=54.9
quant_bytes Q8_0 bytes=146105344 pct=30.1
quant_bytes Q6_K bytes=42900480 pct=8.8
quant_bytes Q4_K bytes=29417472 pct=6.1
quant_bytes F32 bytes=286208 pct=0.1
weight_mib_round=463
kv_f16_bytes_per_token=12288
gen_done ctx=4096 done=True done_reason=length eval_count=1
ps_context_length ctx=4096 value=4096
ps_size_bytes ctx=4096 value=595832339
ps_size_div_1e6 ctx=4096 value=595.8
ps_size_div_1e9 ctx=4096 value=0.596
ps_size_vram_bytes ctx=4096 value=236432916
kv_mib ctx=4096 value=48
RESULT ctx=4096 weight_mib=463 kv_mib=48 sum_mib=511 ollama_mib=568 gap_mib=57
gen_done ctx=32768 done=True done_reason=length eval_count=1
ps_context_length ctx=32768 value=32768
ps_size_bytes ctx=32768 value=1048303369
ps_size_div_1e6 ctx=32768 value=1048.3
ps_size_div_1e9 ctx=32768 value=1.048
ps_size_vram_bytes ctx=32768 value=236432916
kv_mib ctx=32768 value=384
RESULT ctx=32768 weight_mib=463 kv_mib=384 sum_mib=847 ollama_mib=1000 gap_mib=153
readme - Number of Parameters: 0.49B
readme - Number of Paramaters (Non-Embedding): 0.36B
readme - Number of Layers: 24
readme - Number of Attention Heads (GQA): 14 for Q and 2 for KV
readme - Context Length: Full 32,768 tokens and generation 8192 tokens
SOLVED gguf_bits_and_ollama_size
LLM 메모리 계산 실행 화면: ctx별 가중치·KV 합계와 Ollama 크기 차이를 57·153 MiB로 계산합니다
2026-10-14 14:26:46 KST, Debian 13 — ctx별 가중치·KV 합계와 Ollama 크기 차이를 57·153 MiB로 계산합니다.

캡처와 환경 표는 Debian GNU/Linux 13 (trixie), Linux 6.12.94+ x86_64에서 수행한 실행을 정리합니다. Python 3.13.5, Ollama 0.40.3, gguf 0.19.0을 사용했고, nproc는 8, Mem total은 15Gi로 나왔습니다. 화면의 컨텍스트별 결과에서는 계산 합과 Ollama 적재 크기 사이에 57MiB와 153MiB의 차이가 남습니다. 운영 서버와 비교할 때도 전체 메모리뿐 아니라 실제 적재 방식과 컨텍스트 설정을 함께 기록해야 이 차이를 해석할 수 있습니다.

항목값
운영체제Debian GNU/Linux 13 (trixie)
커널Linux 6.12.94+ x86_64
PythonPython 3.13.5
Ollamaollama version is 0.40.3
gguf0.19.0
nproc8
Mem total15Gi
실행일2026-10-14

GGUF 아키텍처는 qwen2입니다. 층은 24, 어텐션 헤드는 14, KV 헤드는 2, 임베딩 길이는 896, head_dim은 64, feed_forward_length는 4864, 메타데이터의 context_length는 32768입니다. general_file_type은 15입니다. 공식 config.json과 맞춘 여섯 항목은 모두 equal=True입니다. 항목은 num_hidden_layers 24, num_attention_heads 14, num_key_value_heads 2, hidden_size 896, intermediate_size 4864, max_position_embeddings 32768입니다. config.json에 head_dim 키는 없고, config_has_head_dim은 False입니다. 64는 임베딩 길이를 헤드 수로 나눈 스크립트 출력입니다.

텐서 원소 합 tensor_elements는 630167424이고 텐서 바이트 tensor_bytes는 485452288입니다. 바이트에 8을 곱해 원소 수로 나눈 원값은 bits_per_weight_raw 6.162835710149이고, 소수 셋째 자리로 찍은 bits_per_weight_rounded는 6.163입니다. 평균의 분모는 태그의 630170000이 아니라 이 원소 합입니다. 연 파일의 file_bytes는 491400032이고, 태그 tag_size_bytes는 491400150이라 file_bytes_equal_tag는 False입니다. ollama show의 FROM 블롭은 blob_bytes 491400032로 파일과 같고 file_bytes_equal_blob는 True입니다. 파일에서 텐서 바이트를 뺀 file_minus_tensor_bytes는 5947744입니다. 평균 비트의 분자는 파일 크기가 아니라 텐서 바이트입니다.

token_embd.weight와 output.weight의 원소 수는 각각 136134656입니다. 원소 합에서 한 장을 뺀 elements_minus_one_embedding은 494032768이고, 두 장을 뺀 elements_minus_both_embeddings는 357898112입니다. 평균 비트의 분모는 두 행렬을 모두 넣은 630167424입니다.

타입별 텐서 바이트 비중은 Q5_0이 54.9, Q8_0이 30.1, Q6_K가 8.8, Q4_K가 6.1, F32가 0.1입니다. Q4_K 바이트는 29417472입니다. 파일 이름이 Q4_K_M이어도 Q4_K가 텐서 바이트의 대부분을 차지하지 않으므로, 앞자리 4와 측정 평균 6.163이 다른 것이 자연스럽습니다. Q5_0 바이트는 266742784, Q8_0 바이트는 146105344, Q6_K 바이트는 42900480, F32 바이트는 286208입니다.

f16 공식의 토큰당 바이트 kv_f16_bytes_per_token은 12288입니다. 스크립트가 반올림해 찍은 weight_mib_round는 463입니다. 컨텍스트 4096의 kv_mib는 48입니다. 이 호출의 done은 True이고 done_reason은 length이며 eval_count는 1입니다. /api/ps의 context_length도 4096입니다.

4096의 ps_size_bytes는 595832339입니다. 1e6으로 나눈 값은 595.8이고 1e9로 나눈 값은 0.596입니다. ollama ps의 SIZE 열은 595 MB입니다. 이 컨텍스트의 성공 줄은 RESULT ctx=4096 weight_mib=463 kv_mib=48 sum_mib=511 ollama_mib=568 gap_mib=57입니다.

32768도 done이 True이고 done_reason은 length이며 eval_count는 1입니다. ps_context_length는 32768입니다. ps_size_bytes는 1048303369입니다. 1e6으로 나눈 값은 1048.3이고 1e9로 나눈 값은 1.048입니다. ollama ps의 SIZE 열은 1.0 GB입니다. kv_mib는 384입니다. 이 컨텍스트의 성공 줄은 RESULT ctx=32768 weight_mib=463 kv_mib=384 sum_mib=847 ollama_mib=1000 gap_mib=153입니다. 두 컨텍스트의 ps_size_vram_bytes는 모두 236432916입니다. 가중치 463MiB에 f16 KV를 더해도 ollama_mib와 같지 않고, 차이는 4096에서 57MiB, 32768에서 153MiB입니다.

ollama ps의 PROCESSOR 열은 4096에서 60%/40% CPU/GPU이고, 32768에서 77%/23% CPU/GPU입니다. 이 서버에는 GPU가 없는데 GPU 몫이 찍혀서 원인을 따로 확인했습니다. 2026-10-17 16:33 KST에 같은 모델로 두 요청을 다시 보내자 비율과 SIZE가 똑같이 나왔고, 그때의 서버 로그와 CPU 플래그는 아래와 같습니다.

$ grep -E "inference compute|model buffer size" serve.log | head -3
time=2026-10-17T16:33:26.105+09:00 level=INFO source=types.go:50 msg="inference compute" id=cpu library=cpu compute="" name=cpu description=cpu libdirs=ollama driver="" pci_id="" type="" total="15.6 GiB" available="8.7 GiB"
load_tensors:          AMX model buffer size =   225.48 MiB
load_tensors:          CPU model buffer size =   254.66 MiB
$ grep -o -w -E "amx_(tile|int8|bf16)" /proc/cpuinfo | sort -u
amx_bf16
amx_int8
amx_tile
$ python3 -c "print(225.48*1024*1024, 236432916/595832339, 236432916/1048303369)"
236432916.48 0.39681115059449634 0.2255386398553108

장치 목록에 잡힌 것은 id=cpu library=cpu 하나뿐입니다. 대신 이 Xeon CPU는 amx_tile과 amx_int8을 지원하고, llama.cpp는 가중치 일부를 AMX용 버퍼 225.48 MiB에 따로 올립니다. 225.48 MiB를 바이트로 바꾸면 size_vram의 236432916과 맞고, 이 값을 각 size로 나눈 0.397과 0.226이 PROCESSOR 열의 40%와 23%입니다. 표의 GPU 몫은 GPU가 아니라 CPU 백엔드의 AMX 버퍼를 Ollama가 장치 메모리 쪽으로 세어 보여 준 값입니다. Ollama FAQ는 이 열이 모델이 올라간 메모리 위치를 보여 준다고 설명하지만, 이 서버에서는 글자 그대로 읽으면 안 됩니다. 용량 비교에는 이 비율을 쓰지 않고 /api/ps의 size를 씁니다.

스크립트가 README에서 골라 찍은 줄은 매개변수 0.49B, 비임베딩 0.36B, 층 24, Q 헤드 14와 KV 헤드 2, 컨텍스트 32,768토큰과 생성 8192토큰입니다. 비임베딩 줄의 원문 철자는 Paramaters입니다. 0.49B와 0.36B는 그 문서에 적힌 표기입니다. 파일에서 센 원소 수는 tensor_elements 630167424이고, 임베딩 두 장을 뺀 elements_minus_both_embeddings는 357898112입니다.

텐서 바이트와 원소 수로 평균 비트를 구하면 양자화 이름에 휘둘리지 않고, 층과 KV 헤드로 f16 KV를 떼어 낸 뒤 실제 size와 나란히 놓을 수 있습니다. 대가는 gguf 패키지가 있어야 하고 모델을 한 번씩 올려야 한다는 점입니다. 그렇게 해도 가중치 463MiB에 f16 KV를 더한 값은 ollama_mib와 같지 않았고, 차이는 4096에서 57MiB, 32768에서 153MiB였습니다.

file_type 열거값과 관측 증가량을 대조하는 방법

실무에서 한 번 더 헷갈리는 지점은 general.file_type 정수입니다. 이 파일의 값은 15인데, 15를 비트 수로 곱하면 평균이 아니라 다른 종류의 과대 추정이 됩니다. 점검 스크립트는 설치된 gguf의 LlamaFileType으로 그 정수를 이름으로 바꾸고, 같은 공식을 유지한 채 4096과 32768을 다시 올립니다. 두 호출도 같은 flock 파일로 generate와 /api/ps를 한 구간에 두고, size 차이를 토큰 차이로 나눕니다. q8_0과 q4_0 줄은 서버 설정을 바꾼 측정이 아닙니다. FAQ가 f16의 약 1/2, 약 1/4라고 적은 비율을 토큰당 12288바이트에 곱해 비교용으로 찍습니다. 이 스크립트의 마지막 줄은 SOLVED file_type_enum_and_kv_delta입니다.

import json, gguf
path = "/home/user/hands/gguf/qwen2.5-0.5b-instruct-q4_k_m.gguf"
reader = gguf.GGUFReader(path)
arch = reader.fields["general.architecture"].contents()
layers = int(reader.fields[f"{arch}.block_count"].contents())
heads = int(reader.fields[f"{arch}.attention.head_count"].contents())
kv_heads = int(reader.fields[f"{arch}.attention.head_count_kv"].contents())
embed = int(reader.fields[f"{arch}.embedding_length"].contents())
file_type = int(reader.fields["general.file_type"].contents())
enum_name = gguf.LlamaFileType(file_type).name
head_dim = embed // heads
per_tok = 2 * layers * kv_heads * head_dim * 2
n_elem = sum(int(t.n_elements) for t in reader.tensors)
as_bits = n_elem * file_type // 8
print(f"general_file_type={file_type}")
print(f"llama_file_type_name={enum_name}")
print(f"file_type_is_mostly_q4_k_m={enum_name == 'MOSTLY_Q4_K_M'}")
print(f"file_type_if_used_as_bits={file_type}")
print(f"file_type_as_bits_estimate_bytes={as_bits}")
print(f"kv_f16_bytes_per_token={per_tok}")
print(f"faq_half_of_f16_bytes_per_token={per_tok / 2:.1f}")
print(f"faq_quarter_of_f16_bytes_per_token={per_tok / 4:.1f}")

def size_of(ctx):
    ps = json.load(open(f"tmp/c_ps_{ctx}.json"))
    model = [x for x in ps["models"] if str(x.get("name","")).startswith("qwen2.5-0.5b-local")][0]
    gen = json.load(open(f"tmp/c_gen_{ctx}.json"))
    print(f"c_gen_done ctx={ctx} done={gen.get('done')} done_reason={gen.get('done_reason')} eval_count={gen.get('eval_count')}")
    print(f"c_ps_context_length ctx={ctx} value={model.get('context_length')}")
    print(f"c_ps_size_bytes ctx={ctx} value={model['size']}")
    print(f"c_ps_size_vram_bytes ctx={ctx} value={model['size_vram']}")
    return int(model["size"]), int(model["size_vram"])

s_lo, v_lo = size_of(4096)
s_hi, v_hi = size_of(32768)
delta = s_hi - s_lo
dtok = 32768 - 4096
print(f"observed_size_delta_bytes={delta}")
print(f"observed_token_delta={dtok}")
print(f"observed_bytes_per_token={delta / dtok:.3f}")
print(f"observed_minus_f16_per_token={delta / dtok - per_tok:.3f}")
print(f"size_vram_bytes_equal={v_lo == v_hi}")
print(f"size_vram_bytes={v_lo}")

다음 출력은 열거형 해석과 컨텍스트별 차이 계산을 한 번 더 대조한 결과입니다. general_file_type의 15는 MOSTLY_Q4_K_M으로 해석되며, 관측 증가량은 토큰당 15780.937바이트입니다. 뒤에 붙은 FAQ 발췌는 컨텍스트와 KV 자료형의 설정 위치를 설명하므로, 실제 적재 측정 줄과 구분해 읽어야 합니다.

+ date +%F
2026-10-14
+ test -x venv/bin/python
+ curl -sI --max-time 20 --url https://docs.ollama.com/faq
HTTP/2 200 
+ curl -sI --max-time 20 --url https://github.com/ollama/ollama/blob/main/docs/faq.mdx
HTTP/2 200 
+ curl -sI --max-time 20 --url https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct/raw/main/config.json
HTTP/2 200 
+ curl -sI --max-time 20 --url https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct/raw/main/README.md
HTTP/2 200 
+ curl -sI --max-time 20 --url https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct
HTTP/2 200 
+ curl -sI --max-time 20 --url https://github.com/ggml-org/llama.cpp/blob/master/gguf-py/gguf/constants.py
HTTP/2 200 
+ curl -sI --max-time 20 --url https://raw.githubusercontent.com/ggml-org/llama.cpp/master/gguf-py/gguf/constants.py
HTTP/2 200 
+ curl -fsS -m 20 -o tmp/faq.mdx https://raw.githubusercontent.com/ollama/ollama/main/docs/faq.mdx
+ curl -fsS -m 20 -o tmp/constants.py https://raw.githubusercontent.com/ggml-org/llama.cpp/master/gguf-py/gguf/constants.py
+ flock 9
+ curl -sS -m 25 http://127.0.0.1:11434/api/generate num_ctx=4096
+ curl -sS -m 10 http://127.0.0.1:11434/api/ps
+ python3 -c check ps context 4096
+ flock 9
+ curl -sS -m 25 http://127.0.0.1:11434/api/generate num_ctx=32768
+ curl -sS -m 10 http://127.0.0.1:11434/api/ps
+ python3 -c check ps context 32768
+ venv/bin/python -
general_file_type=15
llama_file_type_name=MOSTLY_Q4_K_M
file_type_is_mostly_q4_k_m=True
file_type_if_used_as_bits=15
file_type_as_bits_estimate_bytes=1181563920
kv_f16_bytes_per_token=12288
faq_half_of_f16_bytes_per_token=6144.0
faq_quarter_of_f16_bytes_per_token=3072.0
c_gen_done ctx=4096 done=True done_reason=length eval_count=1
c_ps_context_length ctx=4096 value=4096
c_ps_size_bytes ctx=4096 value=595832339
c_ps_size_vram_bytes ctx=4096 value=236432916
c_gen_done ctx=32768 done=True done_reason=length eval_count=1
c_ps_context_length ctx=32768 value=32768
c_ps_size_bytes ctx=32768 value=1048303369
c_ps_size_vram_bytes ctx=32768 value=236432916
observed_size_delta_bytes=452471030
observed_token_delta=28672
observed_bytes_per_token=15780.937
observed_minus_f16_per_token=3492.937
size_vram_bytes_equal=True
size_vram_bytes=236432916
FAQ_SECTION_BEGIN
## How can I specify the context window size?

By default, Ollama uses a context window size of 4096 tokens.

This can be overridden with the `OLLAMA_CONTEXT_LENGTH` environment variable. For example, to set the default context window to 8K, use:

```shell
OLLAMA_CONTEXT_LENGTH=8192 ollama serve
```

To change this when using `ollama run`, use `/set parameter`:

```shell
/set parameter num_ctx 4096
```

When using the API, specify the `num_ctx` parameter:

```shell
curl http://localhost:11434/api/generate -d '{
  "model": "llama3.2",
  "prompt": "Why is the sky blue?",
  "options": {
    "num_ctx": 4096
  }
}'
```
FAQ_SECTION_END
[GPU 적재 여부 확인 절 생략]
FAQ_SECTION_BEGIN
## How can I set the quantization type for the K/V cache?

The K/V context cache can be quantized to significantly reduce memory usage when Flash Attention is enabled.

To use quantized K/V cache with Ollama you can set the following environment variable:

- `OLLAMA_KV_CACHE_TYPE` - The quantization type for the K/V cache. Default is `f16`.

<Note>
  Currently this is a global option - meaning all models will run with the
  specified quantization type.
</Note>

The currently available K/V cache quantization types are:

- `f16` - high precision and memory usage (default).
- `q8_0` - 8-bit quantization, uses approximately 1/2 the memory of `f16` with a very small loss in precision, this usually has no noticeable impact on the model's quality (recommended if not using f16).
- `q4_0` - 4-bit quantization, uses approximately 1/4 the memory of `f16` with a small-medium loss in precision that may be more noticeable at higher context sizes.

How much the cache quantization impacts the model's response quality will depend on the model and the task. Models that have a high GQA count (e.g. Qwen2) may see a larger impact on precision from quantization than models with a low GQA count.

You may need to experiment with different quantization types to find the best balance between memory usage and quality.
FAQ_SECTION_END
CONSTANTS_MATCH
    MOSTLY_Q4_K_S        = 14  # except 1d tensors
    MOSTLY_Q4_K_M        = 15  # except 1d tensors
SOLVED file_type_enum_and_kv_delta

위 출력과 다음 캡처는 점검 스크립트가 열거형 이름과 컨텍스트별 적재 크기를 대조한 실행입니다. MOSTLY_Q4_K_M=15를 확인하고 나서도 size 증가량은 f16 KV 계산과 같아지지 않습니다. FAQ 발췌에서 GPU 적재 여부를 확인하는 절은 뺐습니다. 이 서버에는 GPU가 없고, ollama ps의 GPU 몫이 AMX 버퍼라는 점은 앞에서 확인했습니다. 운영 메모리를 추정할 때 문서의 캐시 비율과 이번 실행에서 나온 증가량은 근거의 성격이 다르므로 따로 적어 둡니다.

LLM 메모리 계산 실행 화면: KV 캐시 설명과 MOSTLY_Q4_K_M=15 열거형 값을 구별합니다
2026-10-14 14:27:02 KST, Debian 13 — KV 캐시 설명과 MOSTLY_Q4_K_M=15 열거형 값을 구별합니다.

llama_file_type_name은 MOSTLY_Q4_K_M이고 file_type_is_mostly_q4_k_m은 True입니다. llama.cpp constants.py의 해당 줄도 MOSTLY_Q4_K_M = 15입니다. 15를 비트로 보고 원소 수에 곱한 file_type_as_bits_estimate_bytes는 1181563920입니다. 파일 바이트 491400032보다 큰 값이라, 열거 번호를 비트 폭으로 쓰면 이름 앞자리 4를 쓰는 추정과 반대 방향으로 어긋납니다.

점검 스크립트가 읽은 size는 4096에서 595832339, 32768에서 1048303369입니다. 두 generate 모두 done이 True이고 done_reason은 length이며 eval_count는 1입니다. size 차이 observed_size_delta_bytes는 452471030이고 토큰 차이 observed_token_delta는 28672입니다. 나눈 observed_bytes_per_token은 15780.937입니다. f16 공식 12288과의 차 observed_minus_f16_per_token은 3492.937입니다. 컨텍스트를 키울 때 늘어난 size는 f16 KV 공식보다 큽니다.

FAQ 비율을 곱한 faq_half_of_f16_bytes_per_token은 6144.0이고 faq_quarter_of_f16_bytes_per_token은 3072.0입니다. 문서의 기본 KV 자료형은 f16이고, 이번 스크립트는 OLLAMA_KV_CACHE_TYPE을 넣지 않습니다. 같은 문서는 기본 컨텍스트를 4096토큰으로 적고, API에서는 options.num_ctx로 바꾼다고 적습니다. 이번 호출은 그 필드에 4096과 32768을 넣었습니다.

점검 스크립트의 size_vram_bytes도 236432916으로 같고 size_vram_bytes_equal은 True입니다. 컨텍스트를 4096에서 32768로 올려도 이 필드는 그대로이고, 늘어난 양은 size 쪽에 있습니다. FAQ는 Flash Attention이 켜진 상태에서 KV 캐시 양자화가 메모리를 줄인다고 적습니다. 그 문장은 문서 설명으로만 두고, 이번 글의 숫자는 적재에서 읽은 size와 f16을 가정해 계산한 KV입니다.

문서와 저장소 주소는 점검 스크립트가 curl -sI로 상태 줄을 찍었고, 이번 출력의 상태 줄은 모두 HTTP/2 200입니다. 본문에 건 링크는 그 주소만입니다.

이 방법으로 15를 15비트로 읽는 실수는 막을 수 있고, MOSTLY_Q4_K_M이라는 이름까지 확인됩니다. 컨텍스트를 바꿨을 때의 size 증가도 f16 공식, FAQ 비율과 같은 단위로 견줄 수 있습니다. 다만 6144.0과 3072.0은 서버의 KV 자료형을 바꿔서 잰 값이 아닙니다. 자료형을 정말 바꾸려면 FAQ의 OLLAMA_KV_CACHE_TYPE 같은 서버 전역 설정이 필요하고, 이번 스크립트는 그 설정을 넣지 않았습니다.

갱신 뒤에도 같은 계산 전제가 유지되는지

운영 설정을 옮길 때는 서버와 라이브러리가 갱신될 것을 염두에 두고 응답 필드와 설정 항목을 함께 관리해야 합니다. 이번 계산도 config.json의 head_dim 키를 바로 읽지 않고, GGUF의 임베딩 길이와 헤드 수로 필요한 값을 구했습니다. 갱신한 뒤에는 같은 방식으로 열거형 이름과 모델 구조의 대응 관계를 다시 맞춰 보기를 권합니다. 설정 파일에 키가 있는지, API 응답 형식이 그대로인지를 보는 절차를 배포 과정에 넣어 두면 메모리 계산의 전제가 바뀐 순간을 놓치지 않습니다.

헬스체크는 서버가 응답하는지와 모델이 원하는 컨텍스트로 준비됐는지를 나눠 판단해야 합니다. 모델 준비 상태를 보는 k8s probe라면 /api/ps의 모델 이름과 context_length를 확인하고, 빈 배열을 곧바로 서버 장애로 보지 않게 합니다. 이번 코드처럼 다른 요청이 러너를 바꾸는 상황을 감안해 적재 작업과 상태 조회의 순서를 맞추고, 무거운 /api/generate 요청은 주기적인 생존 확인과 떼어 둡니다. 설정 경로도 API 요청의 options.num_ctx와 서버 환경변수 OLLAMA_CONTEXT_LENGTH, OLLAMA_KV_CACHE_TYPE을 구분해 배포 설정과 함께 관리합니다.

가중치와 KV 캐시를 따로 잡는 메모리 산정

아래 표는 이름 기반 추정에서 텐서 실측과 적재 크기 비교로 판단을 바꾸는 과정을 모았습니다. 메비바이트 행은 해결 스크립트의 RESULT 줄이며, 컨텍스트를 키우면 계산 합과 관측값의 차이도 커집니다. SOLVED gguf_bits_and_ollama_size는 계산 절차가 끝났다는 표시이고, 가중치와 KV의 합만으로 런타임 크기를 설명한다는 뜻은 아닙니다.

방법이번 실행결과
Q숫자만 허용하는 이름 검사재현 스크립트Q4_K_M에서 ValueError
앞자리 4비트 곱재현 스크립트315085000바이트, 태그보다 176315150바이트 작음
텐서 바이트로 평균 비트해결 스크립트반올림 6.163, 원값 6.162835710149
가중치 MiB와 f16 KV, 컨텍스트 4096해결 스크립트463, KV 48, 합 511, size 568, 차이 57
가중치 MiB와 f16 KV, 컨텍스트 32768해결 스크립트463, KV 384, 합 847, size 1000, 차이 153
file_type 15를 열거형으로 읽기점검 스크립트MOSTLY_Q4_K_M
file_type 15를 비트로 곱하기점검 스크립트1181563920바이트
size 증가를 토큰 증가로 나누기점검 스크립트15780.937바이트/토큰, f16 공식보다 3492.937 큼

파일 용량을 양자화 이름으로만 잡아야 한다면, 재현 스크립트의 앞자리 곱 대신 해결 스크립트의 bits_per_weight_rounded와 quant_bytes를 쓰기를 권장합니다. 이 파일의 Q4_K 비중은 6.1이라 이름 앞자리와 평균이 다릅니다. 컨텍스트 4096에서는 가중치 463MiB와 KV 48MiB를 더한 511MiB에도 gap_mib 57이 남고, 32768에서는 KV 384MiB를 더한 합 847MiB와 size 1000MiB 사이가 153MiB 벌어집니다. 적재 크기는 결국 /api/ps의 size로 확인해야 합니다. 컨텍스트를 바꾸는 경우라면 점검 스크립트의 size 증가량을 f16 계산값과 비교합니다.

KV를 더 줄이려는 경우에는 Ollama FAQ의 OLLAMA_KV_CACHE_TYPE을 참고합니다. 문서의 q8_0과 q4_0은 f16의 약 1/2와 약 1/4이고, 점검 스크립트가 그 비율로 찍은 값은 6144.0과 3072.0입니다. 이 값은 문서 비율을 적용한 계산입니다.

해결·점검 스크립트의 숫자를 놓고 보면, 이 모델의 용량 산정은 양자화 이름을 해석하는 일이라기보다 파일의 숫자와 런타임의 숫자를 갈라 보는 일이었습니다. 가중치는 텐서 합계로 기록하고, 운영 요청의 num_ctx를 고정한 뒤 /api/ps의 적재 크기를 함께 남기는 구성을 권합니다. 저라면 컨텍스트 4096에서 기준을 잡고, 긴 입력이 필요한 작업은 32768의 적재 결과로 따로 메모리 예산을 세우겠습니다. 실시간 서비스에 올릴 때는 이번 환경의 15Gi와 nproc 8만 보고 처리 성능을 판단하지 않고, 실제 요청의 지연과 동시 실행 중 메모리 여유를 배포 판단에 넣겠습니다. CPU만으로 지연 목표를 맞추기 어렵다면 GPU가 있는 서버로 옮기는 구성이나 긴 컨텍스트 작업을 별도 처리 경로로 보내는 방안을 검토하겠습니다. KV 양자화를 고를 때도 문서 비율로 예산을 못 박기보다, 서버 전역 설정이 어디까지 적용되는지 확인하고 적재 크기와 응답 품질을 다시 비교하는 순서가 맞다고 봅니다.

Similar Posts

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다