<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>VLLM · Nhân Tài Đức</title>
    <link>https://jared.lynskey.co.nz/vi/tags/vllm/</link>
    <description>Những câu chuyện ngắn về kỹ thuật phần mềm và lãnh đạo đội ngũ kỹ sư — performance review, tuyển dụng và mở rộng team, CI/CD, giám sát DevOps và AI agent. Viết từ Seoul.</description>
    <generator>Hugo</generator>
    <language>vi</language>
    <copyright>© 2026 Nhân Tài Đức</copyright>
    <lastBuildDate>Fri, 30 Oct 2026 18:20:44 +0900</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/vi/tags/vllm/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>So sánh vLLM và Ollama: khi nào nên dùng cái nào để tự triển khai LLM</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2026/2026-10-30-vllm-vs-ollama/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2026/2026-10-30-vllm-vs-ollama/</guid>
      <pubDate>Fri, 30 Oct 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>vLLM</category>
      <category>Ollama</category>
      <category>LLM</category>
      <category>Self-Hosted</category>
      <category>MLOps</category>
      <category>DevOps</category>
      <category>Docker</category>
      <category>Kubernetes</category>
      <description>So sánh vLLM và Ollama sau khi tôi dùng cả hai: Ollama cho laptop hay công cụ cá nhân, vLLM cho máy chủ GPU phục vụ nhiều yêu cầu. Kèm cách cài đặt, Docker, Kubernetes và những chỗ tôi vấp.</description>
      <content:encoded><![CDATA[<p>Nếu chỉ có một người, hoặc một ứng dụng trên một máy, cần có mô hình chạy được trong vài phút thì hãy dùng Ollama. Còn nếu một máy chủ GPU phải trả lời rất nhiều yêu cầu cùng lúc và thông lượng (throughput) là điều quan trọng thì hãy dùng vLLM. Ollama là công cụ chạy mô hình: nó tải về một mô hình đã lượng tử hóa (quantization), nạp mô hình vào bộ nhớ khi bạn gọi tới và giải phóng khi bạn thôi dùng. vLLM là máy chủ suy luận (inference server): ngay lúc khởi động, nó chiếm gần hết GPU, rồi giữ cho GPU luôn bận bằng cách gộp mọi yêu cầu đang xử lý vào chung một lô (batching). Cả hai đều hỗ trợ API của OpenAI, nên đoạn code gọi tới chúng có thể giống hệt nhau.</p>
<p>Ngày nào tôi cũng dùng cả hai. Ollama chạy trên laptop của tôi và trên máy trạm có GPU ở văn phòng, phục vụ việc phát triển và mấy công cụ tôi tự làm cho mình. Còn vLLM thì phục vụ một mô hình chat cho một dự án freelance, trên một GPU 96 GB duy nhất, lúc đầu chạy bằng Docker Compose và bây giờ chạy trong một cụm Kubernetes nhỏ. Bài này nói về thế mạnh của từng công cụ, cách cài đặt chúng, và những chỗ đã khiến tôi mất thời gian ở phía vLLM.</p>

<h2 class="relative group">So sánh nhanh vLLM và Ollama
    <div id="so-sánh-nhanh-vllm-và-ollama" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#so-s%c3%a1nh-nhanh-vllm-v%c3%a0-ollama" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Ollama</th>
					<th>vLLM</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Phù hợp nhất cho</td>
					<td>Một người dùng: laptop, máy dev, công cụ cá nhân</td>
					<td>Nhiều người dùng hoặc một ứng dụng có nhiều yêu cầu, trên máy chủ GPU</td>
			</tr>
			<tr>
					<td>Chạy trên</td>
					<td>macOS, Linux, Windows; Apple silicon, NVIDIA, AMD hoặc CPU</td>
					<td>Suôn sẻ nhất là Linux với GPU NVIDIA; phần cứng khác vẫn chạy được nhưng tốn công hơn</td>
			</tr>
			<tr>
					<td>Mô hình</td>
					<td>GGUF đã lượng tử hóa, lấy từ thư viện riêng của Ollama (<code>qwen3:8b</code>)</td>
					<td>Trọng số (weights) từ Hugging Face ở dạng BF16, FP8, AWQ hoặc GPTQ (<code>Qwen/Qwen3-8B</code>)</td>
			</tr>
			<tr>
					<td>Cài đặt</td>
					<td>Một trình cài đặt, sau đó chạy <code>ollama run</code></td>
					<td><code>pip install vllm</code>, hoặc Docker image <code>vllm/vllm-openai</code></td>
			</tr>
			<tr>
					<td>Bộ nhớ GPU</td>
					<td>Nạp mô hình ở lần dùng đầu tiên, gỡ ra sau 5 phút không hoạt động</td>
					<td>Giữ chỗ một phần cố định của card ngay lúc khởi động (mặc định trên 90%) và không trả lại</td>
			</tr>
			<tr>
					<td>Nhiều yêu cầu cùng lúc</td>
					<td>Vài khe (slot) xử lý song song</td>
					<td>Gộp lô liên tục (continuous batching); đây chính là việc nó được làm ra để làm</td>
			</tr>
			<tr>
					<td>Khởi động</td>
					<td>Vài giây với mô hình nhỏ</td>
					<td>Vài phút với mô hình lớn</td>
			</tr>
			<tr>
					<td>LoRA adapter</td>
					<td>Phải gắn sẵn vào một mô hình mới bằng Modelfile</td>
					<td>Nhiều adapter trên cùng một máy chủ, chọn theo từng yêu cầu</td>
			</tr>
			<tr>
					<td>API</td>
					<td>API riêng, cộng thêm API tương thích OpenAI ở <code>:11434/v1</code></td>
					<td>Tương thích OpenAI ở <code>:8000/v1</code></td>
			</tr>
	</tbody>
</table>

<h2 class="relative group">Ollama là gì
    <div id="ollama-là-gì" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ollama-l%c3%a0-g%c3%ac" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Trong những cách tôi biết, Ollama là cách dễ nhất để có một mô hình trả lời được ngay trên máy của bạn. Nó phát triển từ llama.cpp và chạy các mô hình GGUF đã lượng tử hóa, vì vậy một mô hình 8B chỉ chiếm khoảng 5 GB và chạy thoải mái trên một chiếc MacBook. Bạn tải mô hình về theo tên, mô hình được nạp vào bộ nhớ ở lần gọi đầu tiên, và sau năm phút không có việc gì làm thì nó tự rời khỏi bộ nhớ. Nhờ vậy, Ollama chia sẻ máy một cách lịch sự với mọi thứ khác bạn đang chạy.</p>
<p>Nhưng chính sự lịch sự đó cũng là cái giá phải trả. Ollama được thiết kế xoay quanh việc phục vụ từng người một. Nó có thể trả lời song song vài yêu cầu, nhưng mỗi khe xử lý song song đều tốn thêm bộ nhớ, và ngay từ đầu nó không được làm ra để gộp hàng chục người dùng lên cùng một GPU.</p>

<h2 class="relative group">vLLM là gì
    <div id="vllm-là-gì" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#vllm-l%c3%a0-g%c3%ac" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>vLLM là một engine phục vụ mô hình (serving engine), và nó được làm ra chính là để gộp lô như vậy. Phần lớn sức mạnh của nó đến từ hai ý tưởng. Thứ nhất là PagedAttention: nó lưu KV cache của mỗi yêu cầu (bộ nhớ đệm chứa trạng thái attention mà mô hình tích lũy dần trong lúc đọc và sinh token) thành những khối nhỏ có kích thước cố định, nhờ đó bộ nhớ GPU không bị hao phí vì phần đệm (padding) và phân mảnh. Thứ hai là gộp lô liên tục: yêu cầu mới được đưa vào lô giữa các bước sinh token, thay vì phải chờ lô hiện tại chạy xong. Kết hợp hai ý tưởng này, một GPU chạy vLLM phục vụ được số người dùng đồng thời nhiều hơn hẳn so với chính GPU đó khi xử lý lần lượt từng yêu cầu một.</p>
<p>Cái giá phải trả là vLLM cư xử như thể nó là chủ của cả card GPU. Vừa khởi động, nó đã chiếm phần bộ nhớ GPU dành cho mình, dùng toàn bộ phần mà trọng số mô hình không cần đến để làm KV cache, và giữ hết chỗ đó dù có ai gọi tới hay không. Nó cũng đòi trọng số đầy đủ từ Hugging Face, hoặc một kiểu lượng tử hóa thân thiện với GPU như FP8, chứ không dùng các tệp GGUF như Ollama.</p>

<h2 class="relative group">Một API, hai máy chủ
    <div id="một-api-hai-máy-chủ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#m%e1%bb%99t-api-hai-m%c3%a1y-ch%e1%bb%a7" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Vì cả hai đều cung cấp API chat completions của OpenAI, code của tôi không cần biết nó đang nói chuyện với bên nào. URL và tên mô hình được lấy từ biến môi trường:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">openai</span> <span class="kn">import</span> <span class="n">OpenAI</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Laptop:      LLM_URL=http://localhost:11434/v1  LLM_MODEL=qwen3:8b</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Máy chủ GPU: LLM_URL=http://localhost:8000/v1   LLM_MODEL=qwen3-8b</span>
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">OpenAI</span><span class="p">(</span><span class="n">base_url</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">&#34;LLM_URL&#34;</span><span class="p">],</span> <span class="n">api_key</span><span class="o">=</span><span class="s2">&#34;not-needed&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">reply</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">chat</span><span class="o">.</span><span class="n">completions</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">&#34;LLM_MODEL&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">messages</span><span class="o">=</span><span class="p">[{</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="s2">&#34;Say hello in Korean.&#34;</span><span class="p">}],</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">reply</span><span class="o">.</span><span class="n">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">content</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Trên laptop, vốn không có GPU mạnh, ứng dụng trỏ tới Ollama. Trên máy chủ thì nó trỏ tới vLLM. Ngoài hai biến đó ra không có gì thay đổi, và tôi có thể phát triển ứng dụng mà không cần bật máy GPU lên chút nào. Ở phía vLLM, <code>--served-model-name</code> đặt cho mô hình một cái tên ngắn, để client không phải gửi nguyên đường dẫn Hugging Face đầy đủ.</p>
<p>Có một điều cần lưu ý, vì rất dễ quên: cùng một mô hình nhưng chạy trên hai bên sẽ không cho ra cùng câu trả lời. <code>qwen3:8b</code> của Ollama là bản lượng tử hóa 4-bit, trong khi trên vLLM bạn thường chạy trọng số 16-bit hoặc FP8. Một prompt đã được tinh chỉnh trên bên này có thể cư xử khác trên bên kia, nên bất cứ thứ gì bạn đang đo, dù là chất lượng câu trả lời, việc mô hình có từ chối trả lời hay không, hay JSON có parse được không, đều phải được kiểm tra trên đúng máy chủ mà môi trường production sẽ thật sự dùng.</p>

<h2 class="relative group">Cách cài đặt Ollama
    <div id="cách-cài-đặt-ollama" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%a1ch-c%c3%a0i-%c4%91%e1%ba%b7t-ollama" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Trên Mac hoặc Windows, hãy tải ứng dụng về từ ollama.com. Trên Linux thì chạy:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -fsSL https://ollama.com/install.sh <span class="p">|</span> sh
</span></span><span class="line"><span class="cl">ollama run qwen3:8b</span></span></code></pre></div></div>
<p>Thế là xong. API giờ đã có ở <code>http://localhost:11434</code>. Nó chỉ lắng nghe trên localhost, trừ khi bạn đặt <code>OLLAMA_HOST</code>, và như thế là đúng điều bạn muốn. Ollama của tôi chạy trên máy trạm ở văn phòng, bind vào <code>127.0.0.1</code>, và tôi truy cập nó từ bất cứ đâu qua một đường hầm (<a href="https://jared.lynskey.co.nz/vi/posts/2026/2026-10-01-cloudflare-warp-dev-network/" >cách tôi thiết lập việc đó bằng Cloudflare WARP</a>).</p>

<h2 class="relative group">Cách cài đặt vLLM
    <div id="cách-cài-đặt-vllm" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%a1ch-c%c3%a0i-%c4%91%e1%ba%b7t-vllm" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Trên Linux, với GPU NVIDIA và driver đời mới:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python -m venv .venv <span class="o">&amp;&amp;</span> <span class="nb">source</span> .venv/bin/activate
</span></span><span class="line"><span class="cl">pip install vllm
</span></span><span class="line"><span class="cl">vllm serve Qwen/Qwen3-8B --served-model-name qwen3-8b --max-model-len <span class="m">16384</span></span></span></code></pre></div></div>
<p>Lần chạy đầu tiên sẽ tải trọng số mô hình về từ Hugging Face. Khi log hiện dòng <code>Application startup complete</code>, lệnh <code>curl localhost:8000/v1/models</code> sẽ liệt kê mô hình, và đoạn Python ở trên chạy được với nó.</p>

<h2 class="relative group">Chạy vLLM bằng Docker
    <div id="chạy-vllm-bằng-docker" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ch%e1%ba%a1y-vllm-b%e1%ba%b1ng-docker" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Với bất cứ thứ gì phải chạy liên tục, tôi dùng image chính thức thay vì cài bằng pip, chủ yếu để CUDA, PyTorch và vLLM đi kèm nhau với phiên bản đã được ghim cố định. Dưới đây là một service Compose dạng chung chung:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">vllm</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">vllm/vllm-openai:v0.30.0  </span><span class="w"> </span><span class="c"># ghim một tag cụ thể; image nặng khoảng 30 GB</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ipc</span><span class="p">:</span><span class="w"> </span><span class="l">host                         </span><span class="w"> </span><span class="c"># PyTorch cần bộ nhớ dùng chung (shared memory)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">&gt;</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      --model Qwen/Qwen3-8B
</span></span></span><span class="line"><span class="cl"><span class="sd">      --served-model-name qwen3-8b
</span></span></span><span class="line"><span class="cl"><span class="sd">      --gpu-memory-utilization 0.85
</span></span></span><span class="line"><span class="cl"><span class="sd">      --max-model-len 16384</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">${HOME}/.cache/huggingface:/root/.cache/huggingface</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;127.0.0.1:8000:8000&#34;</span><span class="w">          </span><span class="c"># truy cập qua ssh -L, không qua mạng LAN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">reservations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">devices</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="l">nvidia</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">count</span><span class="p">:</span><span class="w"> </span><span class="l">all</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">capabilities</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">gpu]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;python3&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-c&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;import urllib.request; urllib.request.urlopen(&#39;http://localhost:8000/health&#39;)&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">90</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">start_period</span><span class="p">:</span><span class="w"> </span><span class="l">120s</span></span></span></code></pre></div></div>
<p>Hãy ghim tag. vLLM thay đổi rất nhanh, các cờ (flag) và giá trị mặc định thay đổi giữa các bản phát hành, và bạn muốn chính mình là người quyết định khi nào những thay đổi đó xảy ra. <code>ipc: host</code> (hoặc một <code>shm_size</code> thật rộng rãi) có sẵn trong hướng dẫn Docker của chính vLLM. Còn lý do <code>start_period</code> dài như vậy chính là điều đầu tiên trong phần tiếp theo.</p>

<h2 class="relative group">Những chỗ khiến tôi vấp khi chạy vLLM
    <div id="những-chỗ-khiến-tôi-vấp-khi-chạy-vllm" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%e1%bb%afng-ch%e1%bb%97-khi%e1%ba%bfn-t%c3%b4i-v%e1%ba%a5p-khi-ch%e1%ba%a1y-vllm" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p><strong>Khởi động mất vài phút.</strong> Nạp trọng số, biên dịch và ghi lại (capture) các CUDA graph mất 190 giây trên máy chủ của tôi, và với một mô hình lớn thì ba đến năm phút là chuyện bình thường. Một bài kiểm tra sức khỏe (health check) viết theo kiểu dành cho ứng dụng web sẽ giết nó trước khi nó kịp sẵn sàng. Trong Compose, điều đó có nghĩa là cần một <code>start_period</code> dài; trong Kubernetes thì cần một startup probe. Nó cũng có nghĩa là mỗi lần khởi động lại là vài phút không có mô hình nào phục vụ, nên đừng khởi động lại một cách tùy tiện.</p>
<p><strong>Bạn cho bao nhiêu bộ nhớ GPU, nó lấy bấy nhiêu, ngay từ lúc khởi động.</strong> <code>--gpu-memory-utilization</code> là tỉ lệ bộ nhớ trên card mà vLLM được phép dùng, ở các bản gần đây mặc định là 0.92, và toàn bộ phần mà trọng số không cần đến sẽ trở thành KV cache. Như vậy không sao nếu vLLM chạy một mình trên card. Card của tôi thì không như vậy, vì có mấy mô hình nhỏ hơn chạy bên cạnh, nên tôi đặt 0.70 trên card 96 GB. Mô hình của tôi, một mô hình hỗn hợp chuyên gia (mixture-of-experts) 35B ở định dạng FP8, chiếm 47.7 GiB khi nạp xong và để lại 15 GiB cho KV cache, quá đủ cho độ dài ngữ cảnh (context) 16k. Nếu vLLM từ chối khởi động vì độ dài chuỗi tối đa không vừa trong KV cache, hãy giảm <code>--max-model-len</code> trước khi đi tìm một GPU lớn hơn.</p>
<p><strong>Mỗi chỗ dành cho LoRA đều tốn bộ nhớ ngay từ đầu.</strong> vLLM có thể phục vụ một mô hình gốc (base model) kèm nhiều LoRA adapter và chuyển qua lại giữa chúng theo từng yêu cầu, dựa vào tên mô hình, điều mà Ollama gần như không làm được. Nhưng lúc khởi động, nó giữ sẵn chỗ cho số adapter bằng <code>--max-loras</code>, mỗi adapter ở rank bằng <code>--max-lora-rank</code>, và trên một mô hình mixture-of-experts thì phần giữ chỗ đó bao gồm cả các expert, nên con số cộng dồn rất nhanh. Bốn adapter ở rank 64 không vừa khi đặt 0.70. Hai adapter thì vừa.</p>
<p><strong>Giới hạn số chuỗi mặc định không hợp với mô hình lai.</strong> Mô hình tôi đang phục vụ trộn các lớp kiểu Mamba vào giữa các lớp attention, và mỗi chuỗi (sequence) cần một khối bộ nhớ đệm riêng cho những lớp đó. Với giá trị <code>--max-num-seqs</code> mặc định của vLLM trên card đó, nó không khởi động được; đặt <code>--max-num-seqs 256</code> là hết lỗi. Nếu một kiến trúc mô hình mới hơn không chịu khởi động, hãy kiểm tra cờ này trước khi nghĩ tới những nguyên nhân cao siêu hơn.</p>
<p><strong>Một Service Kubernetes tên <code>vllm</code> làm vLLM sập.</strong> Đây là vụ tôi khoái nhất. Kubernetes tự chèn biến môi trường cho mọi Service trong cùng namespace, đặt tên theo tên Service, nên một Service tên <code>vllm</code> sẽ đưa cho mọi pod biến <code>VLLM_PORT=tcp://&lt;cluster-ip&gt;:8000</code>. vLLM đọc <code>VLLM_PORT</code> như một thiết lập của chính nó, không hiểu nổi một URL nằm ở đó, và chết. Dĩ nhiên là chết sau khi đã nạp xong toàn bộ mô hình, nên mỗi lần thử tốn ba phút. Tài liệu của vLLM có cảnh báo chuyện này, nhưng tôi chỉ đọc thấy sau khi đã dính. Cách sửa là đặt <code>enableServiceLinks: false</code> trên pod, hoặc đặt cho Service một cái tên khác.</p>
<p>Dưới đây là khung của một Deployment chạy được, đã lược bớt chỉ còn những phần quan trọng:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Recreate              </span><span class="w"> </span><span class="c"># pod cũ phải giải phóng GPU trước</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w"> </span>{<span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w"> </span>{<span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enableServiceLinks</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">  </span><span class="c"># không nhận VLLM_PORT=tcp://... từ Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">vllm/vllm-openai:v0.30.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;--model=Qwen/Qwen3-8B&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--served-model-name=qwen3-8b&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">nvidia.com/gpu</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">32Gi      </span><span class="w"> </span><span class="c"># lúc nạp cần nhiều RAM máy host hơn hẳn lúc phục vụ</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- {<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="nt">shm, mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/dev/shm}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">startupProbe</span><span class="p">:</span><span class="w">          </span><span class="c"># đừng phán xét trước khi mô hình nạp xong</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w"> </span>{<span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="nt">/health, port</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">initialDelaySeconds</span><span class="p">:</span><span class="w"> </span><span class="m">120</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">periodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">failureThreshold</span><span class="p">:</span><span class="w"> </span><span class="m">90</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">livenessProbe</span><span class="p">:</span><span class="w">         </span><span class="c"># khởi động lại là nạp lại từ đầu, nên hãy kiên nhẫn</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w"> </span>{<span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="nt">/health, port</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">periodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">timeoutSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">failureThreshold</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">shm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">emptyDir</span><span class="p">:</span><span class="w"> </span>{<span class="nt">medium</span><span class="p">:</span><span class="w"> </span><span class="nt">Memory, sizeLimit</span><span class="p">:</span><span class="w"> </span><span class="l">16Gi}</span></span></span></code></pre></div></div>
<p>Phần đáng sao chép nhất là các probe. Startup probe chờ hai phút, rồi cho mô hình thêm mười lăm phút nữa để khởi động xong, trước khi Kubernetes coi nó là đã chết. Liveness probe chỉ khởi động lại pod sau khoảng năm phút thất bại liên tục, vì <code>/health</code> có thể phản hồi chậm khi engine đang bận, và dù sao thì mỗi lần khởi động lại cũng tốn một lần nạp lại toàn bộ. <code>Recreate</code> rất quan trọng khi chỉ có một GPU: cập nhật cuốn chiếu (rolling update) sẽ khởi động pod mới trước, và pod đó cứ nằm chờ một GPU mà pod cũ vẫn đang giữ. Giới hạn bộ nhớ trông có vẻ hào phóng, vì nó đúng là hào phóng thật; khi trọng số đã nằm trên GPU, pod của tôi ổn định ở khoảng 7 GiB RAM của máy host, nhưng riêng quá trình nạp thì cần nhiều hơn thế rất nhiều.</p>

<h2 class="relative group">Những điều nên biết về Ollama
    <div id="những-điều-nên-biết-về-ollama" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%e1%bb%afng-%c4%91i%e1%bb%81u-n%c3%aan-bi%e1%ba%bft-v%e1%bb%81-ollama" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Với Ollama thì ít bất ngờ hơn, chủ yếu là vì nó làm ít việc hơn.</p>
<p>Yêu cầu đầu tiên sau một khoảng thời gian không ai dùng sẽ chậm, vì mô hình đã bị gỡ khỏi bộ nhớ và phải nạp lại. <code>OLLAMA_KEEP_ALIVE</code> quy định một mô hình được giữ trong bộ nhớ bao lâu sau yêu cầu cuối cùng; hãy tăng giá trị này lên nếu khoảng chờ đó làm bạn khó chịu, hoặc đặt thành <code>-1</code> trên một máy không làm việc gì khác.</p>
<p>Cửa sổ ngữ cảnh (context window) phụ thuộc vào lượng bộ nhớ GPU bạn có. Dưới 24 GiB, Ollama mặc định chỉ dùng 4k token, ít hơn nhiều so với mức mà phần lớn mô hình hỗ trợ, và một prompt không vừa sẽ bị cắt bớt một cách lặng lẽ chứ không bị từ chối. Nếu câu trả lời có vẻ phớt lờ phần đầu của một prompt dài, hãy đặt <code>OLLAMA_CONTEXT_LENGTH</code> (hoặc <code>num_ctx</code> cho từng yêu cầu) bằng đúng mức bạn thật sự cần.</p>
<p>Nếu bạn chuyển tiếp (forward) một Ollama ở máy từ xa về laptop, đừng đặt nó ở cổng 11434. Đó là cổng mặc định của Ollama, và nếu có lúc một Ollama cục bộ trên laptop khởi động, hai bên sẽ tranh nhau cổng đó, và bạn có thể nói chuyện nhầm với bên kia mà không hề hay biết. Tôi chuyển tiếp Ollama của mình về cổng 11435.</p>
<p>Và Ollama không phải là máy chủ cho nhiều người dùng. Dùng cho một công cụ nội bộ chỉ vài người sử dụng thì ổn; khi lượng truy cập đồng thời thật sự xuất hiện, đó là lúc nên chuyển sang vLLM.</p>

<h2 class="relative group">Cần bao nhiêu bộ nhớ GPU
    <div id="cần-bao-nhiêu-bộ-nhớ-gpu" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%e1%ba%a7n-bao-nhi%c3%aau-b%e1%bb%99-nh%e1%bb%9b-gpu" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Kích thước ước tính, chỉ tính riêng phần trọng số:</p>
<table>
	<thead>
			<tr>
					<th>Kích thước mô hình</th>
					<th>16-bit (BF16)</th>
					<th>8-bit (FP8)</th>
					<th>GGUF 4-bit (Q4_K_M)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>8B</td>
					<td>~16 GB</td>
					<td>~8 GB</td>
					<td>~5 GB</td>
			</tr>
			<tr>
					<td>32B</td>
					<td>~64 GB</td>
					<td>~32 GB</td>
					<td>~20 GB</td>
			</tr>
			<tr>
					<td>70B</td>
					<td>~140 GB</td>
					<td>~70 GB</td>
					<td>~43 GB</td>
			</tr>
	</tbody>
</table>
<p>Tức là khoảng 2 byte cho mỗi tham số ở 16-bit, 1 byte ở 8-bit, và hơn nửa byte một chút ở Q4_K_M. Cộng thêm vào phần trọng số là KV cache, thứ tăng theo độ dài ngữ cảnh và theo số yêu cầu đang được xử lý cùng lúc. Chính yếu tố sau, tức số yêu cầu chạy cùng lúc, là lý do cùng một mô hình lại cần nhiều bộ nhớ hơn hẳn khi chạy trên vLLM phục vụ năm mươi người dùng, so với khi chạy trên Ollama chỉ phục vụ mình bạn.</p>

<h2 class="relative group">Nên dùng cái nào
    <div id="nên-dùng-cái-nào" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#n%c3%aan-d%c3%b9ng-c%c3%a1i-n%c3%a0o" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Nếu bạn chỉ muốn có một mô hình chạy trên máy của chính mình, hãy cài Ollama và dừng ở đó; trong vòng mười phút là bạn đã có thứ trả lời được. Nếu bạn có một máy chủ GPU, cùng với người dùng hoặc một ứng dụng sẽ gọi tới nó suốt cả ngày, hãy dùng vLLM, chấp nhận việc nó khởi động chậm và chiếm nhiều bộ nhớ, rồi cấu hình các probe cho đúng. Rất nhiều hệ thống cuối cùng dùng cả hai, hệ thống của tôi cũng vậy: Ollama ở nơi viết code, vLLM ở nơi các yêu cầu đổ về.</p>
<p>Ngoài hai cái này vẫn còn những lựa chọn khác. SGLang là phương án thay thế vLLM mà người ta hay nhắc tới, nhưng tôi chưa chạy nó trên production, nên tôi để nó ra ngoài bài này thay vì đoán mò. Còn nếu mô hình phải chạy trên điện thoại thì lại quay về llama.cpp, như tôi đã viết trong bài <a href="https://jared.lynskey.co.nz/vi/posts/2026/2026-01-30-on-device-ai/" >chạy LLM ngay trên thiết bị với React Native</a>.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-10-30-vllm-vs-ollama/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
