Deep Learning 8 — Triển khai & tối ưu inference

13 thg 7, 2026 6 lượt xem
#ai
#deep-learning
#deployment
#onnx
#quantization
#inference

Deep Learning 8 — Triển khai & tối ưu inference

Cả series đã đưa bạn đi từ nền tảng mạng nơ-ron cho tới thực hành huấn luyện với PyTorch. Nhưng một mô hình đạt độ chính xác 99% trên tập test mà nằm yên trong notebook thì chưa tạo ra một đồng giá trị nào. Bài cuối này khép lại series bằng câu hỏi thực tế nhất: làm sao đưa mô hình ra khỏi phòng thí nghiệm và phục vụ được cho hàng nghìn request mỗi giây, với độ trễ chấp nhận được và chi phí kiểm soát được?

Đây cũng là nơi thế giới deep learning gặp lại thế giới kỹ thuật hệ thống. Nhiều nguyên tắc trùng với MLOps trong ML cổ điển — đóng gói, versioning, giám sát, rollback — nên chúng ta sẽ tập trung vào những gì đặc thù của DL: mô hình nặng, thường cần GPU, và có cả một kho kỹ thuật tối ưu inference riêng.

1. Từ train tới phục vụ: khoảng cách thực sự

Lúc huấn luyện, bạn quan tâm gradient, loss, độ chính xác trên validation. Lúc phục vụ (serving/inference), mối bận tâm đảo hoàn toàn:

  • Độ trễ (latency) — một request mất bao lâu để có kết quả? p50, p95, p99.
  • Thông lượng (throughput) — hệ thống xử lý được bao nhiêu request mỗi giây?
  • Chi phí — mỗi 1000 lần inference tốn bao nhiêu tiền hạ tầng?
  • Độ ổn định — versioning, rollback, không sập khi tải tăng.

Mô hình lúc train mang theo cả bộ máy tính gradient, optimizer state, các lớp dropout/batchnorm ở chế độ "training". Lúc phục vụ, bạn không cần gì trong số đó. Bước đầu tiên luôn là chuyển mô hình sang chế độ inference và tách nó khỏi code huấn luyện.

2. Đóng gói & serialize mô hình

Có ba cách lưu một mô hình PyTorch, khác nhau về mức độ "tự đứng vững":

  • state_dict — chỉ lưu trọng số (một dictionary tensor). Nhẹ, chuẩn mực, nhưng khi load lại bạn phải có sẵn định nghĩa class model trong code. Đây là cách khuyến nghị cho lưu checkpoint trong quá trình phát triển.
  • TorchScript — serialize cả trọng số lẫn đồ thị tính toán, thành một file tự chứa. Load lại không cần class Python gốc, chạy được trong môi trường C++ (LibTorch), phù hợp production.
  • SavedModel (bên TensorFlow) — tương đương: đóng gói graph + weights + chữ ký hàm, load bằng TF Serving.
import torch

# 1) Lưu state_dict — cần class model khi load (minh hoạ)
torch.save(model.state_dict(), "model_weights.pt")

# load lại:
model = MyModel()                 # phải có định nghĩa class
model.load_state_dict(torch.load("model_weights.pt"))
model.eval()                      # rất quan trọng: tắt dropout, cố định batchnorm

# 2) TorchScript — file tự chứa, không cần class khi load
scripted = torch.jit.script(model)      # hoặc torch.jit.trace(model, example_input)
scripted.save("model_scripted.pt")

model.eval() là dòng dễ bị quên nhất và gây hậu quả âm thầm: nếu để nguyên chế độ train, dropout vẫn ngẫu nhiên tắt nơ-ron và batchnorm dùng thống kê của batch hiện tại thay vì thống kê toàn cục — kết quả inference sẽ nhiễu và không nhất quán giữa các lần gọi.

3. ONNX — cầu nối giữa các framework

Vấn đề: bạn train bằng PyTorch, nhưng đội hạ tầng chạy inference trên một runtime tối ưu riêng, hoặc muốn deploy lên môi trường không có PyTorch. ONNX (Open Neural Network Exchange) là định dạng trung gian chung: bạn export mô hình từ PyTorch/TensorFlow ra .onnx, rồi chạy bằng bất kỳ runtime nào hiểu ONNX (phổ biến nhất là ONNX Runtime).

Lợi ích chính:

  • Interoperability — tách rời framework train khỏi engine phục vụ.
  • Tối ưu runtime — ONNX Runtime tự làm graph optimization (gộp phép tính, loại node thừa) và có nhiều backend tăng tốc (CPU, CUDA, TensorRT...).
  • Nhất quán — cùng một file .onnx chạy được ở nhiều nơi.
import torch

model.eval()
dummy = torch.randn(1, 3, 224, 224)      # input mẫu đúng shape (minh hoạ)

torch.onnx.export(
    model, dummy, "model.onnx",
    input_names=["input"], output_names=["logits"],
    dynamic_axes={"input": {0: "batch"}, "logits": {0: "batch"}},  # cho phép batch động
    opset_version=17,
)

# chạy inference bằng ONNX Runtime, không cần PyTorch
import onnxruntime as ort
import numpy as np

sess = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
out = sess.run(None, {"input": np.random.randn(1, 3, 224, 224).astype(np.float32)})

Lưu ý: dynamic_axes cho phép batch size thay đổi lúc chạy — cần thiết nếu bạn muốn gom nhiều request thành một batch (mục 5). Sau khi export, luôn kiểm tra lại: cho cùng một input, output của mô hình gốc và mô hình ONNX phải khớp trong sai số nhỏ.

4. Serving: batch offline vs real-time API

Có hai kiểu phục vụ, chọn theo yêu cầu nghiệp vụ chứ không theo sở thích kỹ thuật:

Batch (offline). Bạn có sẵn một lượng lớn dữ liệu và không cần kết quả tức thì: chấm điểm rủi ro toàn bộ danh mục khách hàng qua đêm, gán nhãn kho ảnh, tính embedding cho cả bảng. Kiểu này chạy như một job (Airflow, Spark), tối ưu cho throughput, không quan tâm latency từng bản ghi. Rẻ và đơn giản nhất.

Real-time API. Client gửi một (hoặc vài) mẫu và chờ kết quả ngay: nhận diện khuôn mặt lúc eKYC, phát hiện gian lận ngay khi giao dịch phát sinh. Kiểu này cần một service luôn chạy, tối ưu cho latency, và phải chịu được tải biến động.

Các lựa chọn hạ tầng cho real-time:

  • FastAPI + mô hình load sẵn trong RAM — đơn giản, kiểm soát hoàn toàn, phù hợp khi tải vừa phải và bạn muốn tự lo logic tiền/hậu xử lý.
  • TorchServe — server chuyên dụng cho mô hình PyTorch, có sẵn quản lý version, batching, metrics.
  • NVIDIA Triton Inference Server — mạnh nhất về hiệu năng: hỗ trợ nhiều framework (ONNX, TorchScript, TensorRT...), dynamic batching, chạy nhiều model song song trên cùng GPU. Là lựa chọn khi bạn cần vắt kiệt GPU.
from fastapi import FastAPI
from pydantic import BaseModel
import onnxruntime as ort
import numpy as np

app = FastAPI()
# load model MỘT LẦN khi khởi động, không load lại mỗi request (minh hoạ)
sess = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])

class Req(BaseModel):
    features: list[float]

@app.post("/predict")
def predict(req: Req):
    x = np.array([req.features], dtype=np.float32)
    logits = sess.run(None, {"input": x})[0]
    prob = 1.0 / (1.0 + np.exp(-logits[0]))
    return {"score": float(prob.max()), "label": int(prob.argmax())}

Điểm mấu chốt trong code trên: load mô hình một lần lúc khởi động, không load lại mỗi request. Load lại là lỗi hiệu năng nghiêm trọng và phổ biến nhất khi mới serve DL.

Trong thực tế, service này được đóng gói Docker (kèm đúng phiên bản CUDA nếu dùng GPU) rồi triển khai trên Kubernetes để autoscale theo tải, health check, và rolling update. Cách vận hành container/K8s này giống hệt phần đã bàn trong MLOpsđưa LLM vào production — DL không phá vỡ khuôn mẫu đó, chỉ nặng hơn về tài nguyên và thường cần GPU.

5. Tối ưu inference: làm mô hình nhanh và rẻ hơn

Đây là phần đặc thù nhất của DL. Một mô hình FP32 chạy "được" nhưng thường vừa chậm vừa tốn. Các kỹ thuật chính:

Quantization (lượng tử hoá) INT8. Trọng số và/hoặc kích hoạt vốn là số thực 32-bit (FP32) được biểu diễn bằng số nguyên 8-bit (INT8). Kết quả: model nhẹ khoảng 4 lần, tính toán số nguyên nhanh hơn nhiều, đặc biệt lợi trên CPU. Đánh đổi là mất một chút độ chính xác — thường nhỏ, nhưng phải đo lại. Có hai lối:

  • Post-training quantization — lượng tử hoá sau khi train xong, nhanh và dễ, đôi khi cần một tập dữ liệu nhỏ để "hiệu chỉnh" (calibration).
  • Quantization-aware training (QAT) — mô phỏng lượng tử hoá ngay trong lúc train, giữ độ chính xác tốt hơn nhưng tốn công hơn.

Mixed precision FP16. Dùng số thực 16-bit thay vì 32-bit, chủ yếu trên GPU. Giảm một nửa bộ nhớ và tăng tốc rõ rệt trên GPU có nhân chuyên dụng (Tensor Core), với sai lệch độ chính xác thường không đáng kể.

Pruning & distillation. Pruning cắt bỏ những trọng số/kết nối ít quan trọng để mô hình thưa hơn. Distillation (chưng cất) huấn luyện một mô hình nhỏ ("student") học lại từ mô hình lớn ("teacher") — student nhẹ hơn nhiều mà giữ được phần lớn chất lượng. Cả hai đều nhằm giảm kích thước và độ trễ.

Dynamic batching. Thay vì xử lý từng request lẻ, server gom các request đến gần nhau trong một cửa sổ thời gian rất ngắn (vài mili-giây) thành một batch rồi đưa qua GPU một lần. GPU vốn hiệu quả nhất khi xử lý batch lớn, nên cách này tăng throughput mạnh mà chỉ đánh đổi một chút latency. Triton và TorchServe hỗ trợ sẵn.

import torch

# Dynamic quantization — nhanh nhất để thử, hợp lớp Linear/LSTM trên CPU (minh hoạ)
model.eval()
quantized = torch.quantization.quantize_dynamic(
    model, {torch.nn.Linear}, dtype=torch.qint8
)
torch.save(quantized.state_dict(), "model_int8.pt")

# Mixed precision khi inference trên GPU (autocast)
model_gpu = model.to("cuda").eval()
with torch.no_grad(), torch.autocast(device_type="cuda", dtype=torch.float16):
    logits = model_gpu(batch.to("cuda"))

Nguyên tắc vàng: tối ưu là đánh đổi, luôn đo lại cả tốc độ lẫn chất lượng. Đừng bao giờ ship một mô hình đã quantize mà không so lại độ chính xác với bản gốc trên tập test.

6. GPU vs CPU: khi nào cần gì

Không phải cứ DL là phải GPU. Quy tắc thực dụng:

  • CPU đủ dùng khi: mô hình nhỏ/vừa, latency yêu cầu không quá gắt, tải thấp, hoặc mô hình đã quantize INT8. CPU rẻ, dễ scale ngang, không phải tranh giành tài nguyên khan hiếm.
  • Cần GPU khi: mô hình lớn (CNN sâu, transformer), cần latency thấp dưới tải cao, hoặc batch lớn. GPU đắt và thường là nút cổ chai chi phí, nên phải dùng cho đáng.

Điều then chốt về chi phí: GPU chỉ hiệu quả khi được nạp đầy tải. Một GPU xử lý một request mỗi lần thì lãng phí khủng khiếp — đó chính là lý do dynamic batching quan trọng: gom request để lấp đầy GPU, chia chi phí đắt đỏ cho nhiều request. Một GPU chạy 30% công suất thường là dấu hiệu nên chuyển sang CPU hoặc gộp tải.

7. Giám sát trong production

Ship xong không phải là hết. Mô hình DL trong production cần được theo dõi trên bốn trục:

  • Latency — p50/p95/p99, cảnh báo khi p99 vượt ngưỡng SLA.
  • Throughput & tài nguyên — request/giây, mức sử dụng GPU/CPU/RAM.
  • Data drift — phân phối đầu vào có lệch so với lúc train không? Ảnh eKYC ban đầu chụp trong nhà, giờ phần lớn chụp ngoài trời — mô hình có thể vẫn "chạy" nhưng sai nhiều hơn.
  • Concept drift & chất lượng — quan hệ giữa input và output có đổi không, và độ chính xác thực tế (khi có nhãn phản hồi về) có tụt không.

Điểm khó của DL so với ML cổ điển: đầu vào thường là ảnh/âm thanh/văn bản phi cấu trúc, khó "đo drift" trực tiếp. Mẹo thực tế là giám sát drift trên embedding hoặc trên phân phối điểm số đầu ra, thay vì trên pixel thô. Toàn bộ tư duy giám sát này nối liền với phần MLOps đã bàn ở ml-08; DL chỉ thêm gánh nặng "đầu vào phi cấu trúc".

8. Versioning, rollback & edge

Mọi mô hình lên production phải có version rõ ràng (v1, v2...), gắn với đúng dữ liệu và code đã train ra nó. Khi v2 tệ hơn kỳ vọng, bạn phải rollback về v1 trong vài phút — điều này chỉ làm được nếu artifact cũ được lưu và service hỗ trợ trỏ lại version cũ. Kỹ thuật quen thuộc: canary (đẩy v2 cho một phần nhỏ traffic trước) và shadow (chạy v2 song song, so kết quả nhưng chưa phục vụ thật).

Một hướng riêng đáng nhắc là edge/mobile — chạy mô hình ngay trên điện thoại hoặc thiết bị IoT thay vì gọi server. Ưu điểm: không cần mạng, bảo mật dữ liệu tại chỗ, latency cực thấp. Đổi lại phải nén mô hình rất mạnh (quantization, distillation gần như bắt buộc) và dùng runtime chuyên biệt cho thiết bị. Với eKYC trên app ngân hàng, một phần xử lý (phát hiện khuôn mặt, kiểm tra chất lượng ảnh) thường chạy ngay trên máy khách để phản hồi tức thì, còn phần đối sánh nhạy cảm chạy trên server.

9. Sơ đồ vòng đời triển khai

Vòng lặp này — train, export, tối ưu, phục vụ, giám sát, rồi retrain khi cần — chính là "life in production" của một mô hình. Nó không bao giờ thực sự "xong".

10. Khi nào DL, khi nào ML cổ điển, khi nào LLM

Deep learning không phải câu trả lời cho mọi bài toán. Chọn công cụ theo dạng dữ liệu và bài toán:

  • Dữ liệu bảng (tabular) — điểm tín dụng, dự báo churn, phát hiện gian lận trên đặc trưng số: gradient boosting (XGBoost/LightGBM) thường thắng DL, rẻ hơn, dễ giải thích hơn. Xem cây & ensemble. Đừng dùng mạng nơ-ron cho tabular chỉ vì nó "hiện đại".
  • Dữ liệu phi cấu trúc — ảnh, âm thanh, video: đây là sân nhà của DL (CNN, transformer). eKYC, OCR chứng từ, nhận diện chữ ký.
  • Ngôn ngữ tự nhiên phức tạp, cần suy luận/sinh văn bản — tóm tắt, hỏi đáp, phân loại tinh vi: cân nhắc LLM, nhưng nhớ chi phí và độ trễ cao hơn hẳn. Xem đưa LLM vào production.

Quy tắc gọn: bắt đầu từ giải pháp đơn giản nhất chạy được, chỉ leo lên DL/LLM khi bài toán thực sự đòi hỏi và bạn đã đo được lợi ích rõ ràng.

11. Model risk & giải thích (ngân hàng)

Trong ngành ngân hàng, một mô hình ra production còn phải qua cửa quản trị rủi ro mô hình (model risk management). Vài điểm bắt buộc lưu ý:

  • Giải thích được — với quyết định ảnh hưởng khách hàng (từ chối vay, chặn giao dịch), phải giải thích được vì sao. DL vốn là "hộp đen", nên với các quyết định nhạy cảm, hoặc chọn mô hình dễ giải thích hơn (GBM + SHAP), hoặc bổ sung lớp giải thích và quy trình con người phê duyệt.
  • Kiểm soát drift & tái thẩm định — mô hình phải được định kỳ đánh giá lại, có hồ sơ version, dữ liệu, và kết quả kiểm thử.
  • Công bằng & tuân thủ — kiểm tra mô hình không phân biệt đối xử theo các thuộc tính nhạy cảm; lưu vết audit đầy đủ.

Đây không phải rào cản kỹ thuật thuần tuý mà là yêu cầu pháp lý và uy tín — bỏ qua nó có thể khiến cả hệ thống bị dừng dù độ chính xác rất cao.

Use case thực tế: serve mô hình eKYC nhận diện khuôn mặt real-time

Một ngân hàng số cần đối sánh ảnh selfie của khách với ảnh trên CMND/CCCD ngay trong luồng mở tài khoản. Yêu cầu: p95 latency dưới 300ms, chịu tải hàng nghìn phiên eKYC giờ cao điểm, và tuyệt đối không lộ ảnh khách.

Kiến trúc triển khai:

  1. Mô hình. Một CNN trích embedding khuôn mặt (xem CNN), train bằng PyTorch, export sang ONNX để chạy trên ONNX Runtime.
  2. Tối ưu. Quantize/FP16 để giảm latency; bật dynamic batching trên Triton để gom các phiên eKYC đồng thời, lấp đầy GPU. Bước phát hiện khuôn mặt và kiểm tra chất lượng ảnh nhẹ chạy trên app điện thoại (edge) để phản hồi tức thì và giảm ảnh gửi lên.
  3. Phục vụ. Service đóng gói Docker với đúng phiên bản CUDA, chạy trên K8s có autoscale theo tải, health check, và rolling update.
  4. Bảo mật. Ảnh chỉ xử lý trong bộ nhớ, không lưu thô; log không chứa dữ liệu sinh trắc.
  5. Giám sát. Theo dõi latency p95/p99, GPU utilization, và data drift trên embedding (điều kiện chụp thay đổi theo mùa/thiết bị mới). Khi tỷ lệ đối sánh sai vượt ngưỡng, kích hoạt retrain và deploy version mới qua canary.
  6. Rủi ro mô hình. Ngưỡng chấp nhận/từ chối được hội đồng rủi ro phê duyệt; ca biên (điểm gần ngưỡng) chuyển cho nhân viên xét duyệt thủ công — con người vẫn giữ quyền quyết định cuối.

Kết quả là một vòng khép kín: mô hình phục vụ real-time, tự giám sát, và tái huấn luyện khi thực tế đổi thay — đúng tinh thần cả series đã xây dựng.

Ghi nhớ

  • Train và serve là hai thế giới khác nhau. Lúc phục vụ, quan tâm latency, throughput, chi phí, ổn định — không phải loss. Luôn model.eval() và tách khỏi code train.
  • Serialize đúng cách: state_dict cho checkpoint, TorchScript/SavedModel cho artifact tự chứa, ONNX để tách framework train khỏi engine phục vụ và mở khoá tối ưu runtime.
  • Batch cho offline, API real-time cho tức thì. FastAPI khi đơn giản, Triton/TorchServe khi cần vắt kiệt GPU. Load model một lần, đóng Docker, chạy K8s.
  • Tối ưu là đánh đổi: quantization INT8 (rẻ, hợp CPU), FP16 (GPU), pruning/distillation (nhẹ hơn), dynamic batching (lấp đầy GPU). Luôn đo lại cả tốc độ lẫn chất lượng.
  • GPU chỉ đáng khi được nạp đầy tải. Mô hình nhỏ/tải thấp thường nên chạy CPU.
  • Giám sát trên bốn trục: latency, throughput, data drift, chất lượng. Với DL, đo drift trên embedding thay vì trên pixel thô.
  • Chọn đúng công cụ: tabular → GBM; ảnh/âm thanh → DL; ngôn ngữ phức tạp → cân nhắc LLM. Đơn giản trước, leo thang khi thật cần.
  • Trong ngân hàng, model risk là bắt buộc: giải thích được, kiểm soát drift, công bằng, audit — và giữ con người trong vòng quyết định nhạy cảm.

Bài viết liên quan

Phân biệt AI/ML/DL, các kiểu học máy, quy trình ML end-to-end và thuật ngữ nền tảng.

13 thg 7, 2026 14

Chất lượng dữ liệu, xử lý thiếu/ngoại lai, mã hoá, chuẩn hoá, tạo đặc trưng và tránh data leakage.

13 thg 7, 2026 13

Mô hình ngôn ngữ lớn hoạt động ra sao, token, context window, tham số sinh, và kỹ thuật viết prompt.

13 thg 7, 2026 13

Retrieval-Augmented Generation, vector database, tool calling, agent và MCP để xây ứng dụng LLM thực tế.

13 thg 7, 2026 11