บทนำ: ทำไมต้อง Distill และ Quantize DeepSeek R1 671B

ในโลกของ Large Language Models ปี 2026 DeepSeek R1 671B ได้กลายเป็น milestone สำคัญด้วยความสามารถ reasoning ระดับเทียบเท่า GPT-4.1 แต่ด้วยขนาด 671 พันล้าน parameters การ deploy บน infrastructure ทั่วไปแทบเป็นไปไม่ได้ ผมเคยทดลอง load โมเดลขนาดเต็มบน server 256GB RAM ผลลัพธ์คือ OOM (Out Of Memory) error ในทันที หลังจากนั้นผมจึงเริ่มศึกษาเชิงลึกเกี่ยวกับเทคนิค Distillation และ Quantization อย่างจริงจัง เพื่อลดขนาดโมเดลลงอย่างมีนัยสำคัญโดยยังคงความสามารถหลักเอาไว้ บทความนี้จะพาทุกท่านไปทำความเข้าใจสถาปัตยกรรมของ DeepSeek R1, กระบวนการ Knowledge Distillation, วิธีการ Quantization ตั้งแต่ระดับ INT8 ถึง INT4, รวมถึงการ optimize สำหรับ production deployment พร้อมโค้ดที่พร้อมใช้งานจริง ผมจะแชร์ประสบการณ์ตรงจากการทดลองและ error ต่างๆ ที่พบเจอระหว่างทาง รวมถึงวิธีแก้ไขที่ได้ผล สำหรับผู้ที่ต้องการทดสอบ DeepSeek R1 ผ่าน API โดยไม่ต้อง setup infrastructure เอง ผมแนะนำ สมัครที่นี่ เพื่อใช้งาน HolySheep AI ซึ่งมี latency เฉลี่ยต่ำกว่า 50ms และอัตราค่าบริการที่ประหยัดกว่า 85% เมื่อเทียบกับบริการอื่น พร้อมรองรับ WeChat และ Alipay

DeepSeek R1 671B Architecture ภาพรวมและหลักการ

DeepSeek R1 671B ใช้สถาปัตยกรรม Mixture of Experts (MoE) ที่แตกต่างจากโมเดล dense ทั่วไปอย่างสิ้นเชิง MoE architecture ช่วยให้โมเดลมีจำนวน parameters มหาศาลแต่ใช้ active parameters เพียงส่วนน้อยในแต่ละ forward pass ซึ่งเป็นหัวใจสำคัญที่ทำให้การ inference มีความเป็นไปได้ในทางปฏิบัติ สถาปัตยกรรมหลักประกอบด้วย 256 experts โดยในแต่ละ token จะ activate เพียง 8 experts เท่านั้น ทำให้ effective parameters อยู่ที่ประมาณ 21 พันล้าน parameters ต่อ forward pass นี่คือสาเหตุที่แม้จะเป็น 671B โมเดล แต่ VRAM requirement สำหรับ inference อยู่ที่ประมาณ 320GB สำหรับ FP16 ซึ่งยังคงเป็นความท้าทายสำหรับ hardware ส่วนใหญ่ ความพิเศษอีกอย่างของ DeepSeek R1 คือระบบ GRPO (Group Relative Policy Optimization) ที่ใช้ในการ train โมเดลให้มีความสามารถในการ reasoning ขั้นสูง โมเดลสามารถแสดง thought process ก่อนตอบคำถาม ทำให้เหมาะสำหรับงานที่ต้องการความแม่นยำและความ透明

Knowledge Distillation: หลักการและวิธีการ

Knowledge Distillation คือกระบวนการถ่ายโอน "ความรู้" จากโมเดลใหญ่ (teacher) ไปยังโมเดลเล็ก (student) โดยมีหลักการว่าแม้โมเดลเล็กจะไม่สามารถเรียนรู้ทุกอย่างจากโมเดลใหญ่ได้ แต่สามารถเรียนรู้ "การกระจายความน่าจะเป็น" ของ output ได้ดีกว่าการ train จาก hard labels เพียงอย่างเดียว กระบวนการ distillation ที่มีประสิทธิภาพประกอบด้วย 3 ขั้นตอนหลัก ขั้นตอนแรกคือการ generate dataset จาก teacher model โดยใช้ temperature สูงเพื่อให้ได้ response ที่หลากหลาย ขั้นตอนที่สองคือการ fine-tune student model ด้วย dataset ที่ได้ โดยใช้ loss function ที่รวม soft targets จาก teacher และ hard targets จาก ground truth ขั้นตอนสุดท้ายคือการ evaluate และ iterate เพื่อปรับปรุงคุณภาพ
import torch
import torch.nn.functional as F
from torch.distributions import Categorical

class DistillationLoss:
    """
    Knowledge Distillation Loss for DeepSeek R1
    Combines soft targets (teacher distribution) with hard targets
    """
    def __init__(self, temperature=4.0, alpha=0.7):
        self.temperature = temperature
        self.alpha = alpha  # Weight for soft targets
        
    def compute_loss(self, student_logits, teacher_logits, hard_labels):
        # Soft targets loss (KL Divergence)
        student_soft = F.log_softmax(student_logits / self.temperature, dim=-1)
        teacher_soft = F.softmax(teacher_logits / self.temperature, dim=-1)
        soft_loss = F.kl_div(student_soft, teacher_soft, reduction='batchmean')
        soft_loss = soft_loss * (self.temperature ** 2)
        
        # Hard targets loss (Cross Entropy)
        hard_loss = F.cross_entropy(student_logits, hard_labels)
        
        # Combined loss
        total_loss = self.alpha * soft_loss + (1 - self.alpha) * hard_loss
        return total_loss
    
    def generate_distilled_dataset(self, teacher_model, dataset, batch_size=8):
        """
        Generate training dataset from teacher model
        Returns (prompt, response, teacher_distribution) tuples
        """
        distilled_data = []
        teacher_model.eval()
        
        with torch.no_grad():
            for i in range(0, len(dataset), batch_size):
                batch = dataset[i:i+batch_size]
                prompts = [item['prompt'] for item in batch]
                
                # Generate with high temperature for diversity
                teacher_outputs = teacher_model.generate(
                    prompts, 
                    temperature=1.2,
                    do_sample=True,
                    max_new_tokens=2048
                )
                
                for prompt, output in zip(prompts, teacher_outputs):
                    distilled_data.append({
                        'prompt': prompt,
                        'response': output,
                        'teacher_logits': output.get('logits', None)
                    })
                    
        return distilled_data

Usage Example

distiller = DistillationLoss(temperature=6.0, alpha=0.8) distilled_dataset = distiller.generate_distilled_dataset( teacher_model=deepseek_r1_671b, dataset=training_data ) print(f"Generated {len(distilled_dataset)} distilled samples")

Quantization: ลดขนาดโมเดลด้วย INT8 และ INT4

Quantization เป็นเทคนิคที่ลด precision ของ weights จาก FP32 หรือ FP16 ไปเป็น INT8 หรือ INT4 โดยมีหลักการว่า neural networks มีความ tolerant ต่อ noise สูง และ precision ที่ลดลงมักไม่ส่งผลกระทบอย่างมีนัยสำคัญต่อความแม่นยำในงานส่วนใหญ่ การ quantize DeepSeek R1 671B จาก FP16 (~1.3TB) ไปเป็น INT4 สามารถลดขนาดลงเหลือประมาณ 170GB ซึ่งเป็นขนาดที่ deploy ได้บน 4x A100 80GB มี 2 วิธีหลักในการ quantize คือ Post-Training Quantization (PTQ) ที่ quantize โมเดลที่ train แล้วโดยไม่ต้อง retrain ซึ่งเร็วและง่าย แต่อาจมี accuracy loss บ้าง และ Quantization-Aware Training (QAT) ที่ train โมเดลด้วย quantization-aware process ตั้งแต่ต้น ซึ่งรักษา accuracy ได้ดีกว่าแต่ใช้เวลาและทรัพยากรมากกว่า สำหรับ DeepSeek R1 671B ผมแนะนำให้ใช้ PTQ ด้วย bitsandbytes library หรือ GPTQ/AWQ สำหรับ INT4 quantization เนื่องจากโมเดลมีขนาดใหญ่มากและการ train ใหม่ตั้งแต่ต้นไม่สมเหตุสมผลในทางปฏิบัติ
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

def load_quantized_deepseek(
    model_path: str = "deepseek-ai/DeepSeek-R1-671B",
    quantization: str = "int4",  # Options: "fp16", "int8", "int4"
    load_in_4bit: bool = True,
    bnb_4bit_compute_dtype: torch.dtype = torch.bfloat16,
    bnb_4bit_quant_type: str = "nf4"
):
    """
    Load DeepSeek R1 671B with various quantization levels
    Supports FP16, INT8, and INT4 quantization
    """
    
    if quantization == "int4" and load_in_4bit:
        # 4-bit quantization using bitsandbytes NF4
        bnb_config = BitsAndBytesConfig(
            load_in_4bit=True,
            bnb_4bit_quant_type=bnb_4bit_quant_type,
            bnb_4bit_compute_dtype=bnb_4bit_compute_dtype,
            bnb_4bit_use_double_quant=True,  # Double quantization for memory saving
            llm_int8_threshold=6.0,
            llm_int8_has_fp16_weight=False
        )
        print("Loading DeepSeek R1 671B with INT4 NF4 quantization...")
        
    elif quantization == "int8":
        bnb_config = BitsAndBytesConfig(
            load_in_8bit=True,
            llm_int8_threshold=6.0,
            llm_int8_has_fp16_weight=True
        )
        print("Loading DeepSeek R1 671B with INT8 quantization...")
        
    else:
        # FP16 without quantization
        bnb_config = None
        print("Loading DeepSeek R1 671B in FP16 (full precision)...")
    
    tokenizer = AutoTokenizer.from_pretrained(
        model_path,
        trust_remote_code=True,
        use_fast=False
    )
    
    model = AutoModelForCausalLM.from_pretrained(
        model_path,
        quantization_config=bnb_config,
        device_map="auto",
        trust_remote_code=True,
        torch_dtype=bnb_4bit_compute_dtype if bnb_config else torch.float16,
        low_cpu_mem_usage=True,  # Reduce CPU memory during loading
        max_memory={  # Memory allocation per GPU
            0: "60GB",
            1: "60GB",
            2: "60GB",
            3: "60GB"
        }
    )
    
    model.eval()
    
    # Calculate and display model size
    if hasattr(model, 'get_memory_footprint'):
        memory_footprint = model.get_memory_footprint() / (1024 ** 4)
        print(f"Model memory footprint: {memory_footprint:.2f} TB")
    
    return model, tokenizer

Example usage for different quantization levels

print("=" * 60) print("DeepSeek R1 671B Quantization Comparison") print("=" * 60) configs = [ ("FP16", "fp16"), ("INT8", "int8"), ("INT4-NF4", "int4") ] for name, quant in configs: if quant == "fp16": model, tokenizer = load_quantized_deepseek(quantization="fp16") else: print(f"\n--- Testing {name} quantization ---") # In real scenario, uncomment the line below # model, tokenizer = load_quantized_deepseek(quantization=quant) print(f" Estimated size for {name}: ", end="") if quant == "int8": print("~325GB VRAM") elif quant == "int4": print("~170GB VRAM") print(f" Accuracy retention: ", end="") if quant == "int8": print("~99%") elif quant == "int4": print("~97%")

Advanced Quantization ด้วย GPTQ และ AutoGPTQ

สำหรับ production deployment ที่ต้องการ performance สูงสุด ผมแนะนำให้ใช้ GPTQ (Generative Pretrained Transformer Quantization) ซึ่งให้ผลลัพธ์ดีกว่า naive quantization โดยเฉพาะสำหรับโมเดลขนาดใหญ่ GPTQ ทำงานโดย calibrate กับ dataset จำนวนหนึ่งแล้วหา weight ที่ดีที่สุดสำหรับแต่ละ quantized value
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
from transformers import AutoTokenizer
import torch
from datasets import load_dataset

class DeepSeekGPTQQuantizer:
    """
    Advanced GPTQ Quantization for DeepSeek R1 671B
    Uses calibration dataset for optimal weight selection
    """
    
    def __init__(self, model_path: str, quant_config: dict):
        self.model_path = model_path
        self.quant_config = quant_config
        
    def prepare_calibration_dataset(self, dataset_name: str = "c4", num_samples: int = 512):
        """
        Prepare calibration dataset for GPTQ
        """
        print(f"Loading calibration dataset: {dataset_name}")
        
        if dataset_name == "c4":
            # Use C4 dataset for English text
            from datasets import load_dataset
            data = load_dataset("allenai/c4", "en", split="train", streaming=True)
            data = data.shuffle(seed=42)
        elif dataset_name == "pile":
            # Use The Pile for diverse content
            data = load_dataset("EleutherAI/pile", split="train", streaming=True)
        
        samples = []
        tokenizer = AutoTokenizer.from_pretrained(self.model_path, trust_remote_code=True)
        
        print(f"Collecting {num_samples} calibration samples...")
        for i, example in enumerate(data):
            if i >= num_samples:
                break
                
            text = example.get("text", example.get("content", ""))
            if len(text) > 100:
                # Tokenize and store input IDs
                inputs = tokenizer(
                    text[:2048],  # Truncate long texts
                    return_tensors="pt",
                    truncation=True,
                    max_length=2048,
                    padding=False
                )
                samples.append(inputs.input_ids)
        
        return samples
    
    def quantize_model(
        self, 
        output_path: str,
        bits: int = 4,
        group_size: int = 128,
        desc_act: bool = True
    ):
        """
        Quantize DeepSeek R1 using GPTQ
        """
        print(f"Loading model from {self.model_path}...")
        
        quantize_config = BaseQuantizeConfig(
            bits=bits,
            group_size=group_size,
            desc_act=desc_act,  # Activation order for better accuracy
            true_seqlen=32768  # Max sequence length for DeepSeek R1
        )
        
        model = AutoGPTQForCausalLM.from_pretrained(
            self.model_path,
            quantize_config=quantize_config,
            device_map="auto",
            trust_remote_code=True,
            low_cpu_mem_usage=True,
            torch_dtype=torch.float16
        )
        
        # Prepare calibration data
        calibration_samples = self.prepare_calibration_dataset()
        
        print(f"\nStarting GPTQ quantization with {bits}-bit, group_size={group_size}...")
        print("This may take 2-4 hours for 671B model...")
        
        model.quantize(
            calibration_samples,
            batch_size=1,
            use_triton=False,
            autotune_warmup_after_quantize=True
        )
        
        print("Quantization complete! Saving model...")
        model.save_quantized(output_path)
        
        # Calculate compression ratio
        original_size = 671 * 2  # 671B params * 2 bytes (FP16)
        quantized_size = 671 * bits / 8  # 671B params * bits / 8 bytes
        compression_ratio = original_size / quantized_size
        
        print(f"\nQuantization Summary:")
        print(f"  Original size: {original_size:.1f} GB (FP16)")
        print(f"  Quantized size: {quantized_size:.1f} GB ({bits}-bit)")
        print(f"  Compression ratio: {compression_ratio:.1f}x")
        
        return model
    
    def benchmark_quantized_model(self, model, tokenizer):
        """
        Benchmark quantized model performance
        """
        print("\n" + "=" * 60)
        print("Benchmarking Quantized Model")
        print("=" * 60)
        
        test_prompts = [
            "Explain the concept of quantum entanglement in simple terms.",
            "Write a Python function to implement binary search.",
            "What are the main differences between machine learning and deep learning?"
        ]
        
        results = []
        for prompt in test_prompts:
            inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
            
            import time
            start = time.time()
            
            with torch.no_grad():
                outputs = model.generate(
                    **inputs,
                    max_new_tokens=256,
                    do_sample=True,
                    temperature=0.7,
                    top_p=0.9
                )
            
            latency = time.time() - start
            response = tokenizer.decode(outputs[0], skip_special_tokens=True)
            
            results.append({
                "prompt": prompt[:50] + "...",
                "latency": latency,
                "tokens": len(outputs[0]) - len(inputs.input_ids[0]),
                "tokens_per_second": (len(outputs[0]) - len(inputs.input_ids[0])) / latency
            })
            
        avg_latency = sum(r["latency"] for r in results) / len(results)
        avg_tps = sum(r["tokens_per_second"] for r in results) / len(results)
        
        print(f"\nAverage Latency: {avg_latency:.2f}s")
        print(f"Average Throughput: {avg_tps:.2f} tokens/s")
        
        return results

Usage

quantizer = DeepSeekGPTQQuantizer( model_path="deepseek-ai/DeepSeek-R1-671B", quant_config={"bits": 4, "group_size": 128} )

Quantize and benchmark

quantized_model = quantizer.quantize_model(

output_path="./deepseek-r1-671b-gptq-4bit",

bits=4,

group_size=128

)

Production Deployment: Multi-GPU และ Distributed Inference

การ deploy DeepSeek R1 671B บน production environment ต้องอาศัย multi-GPU setup เนื่องจากขนาดโมเดลที่ใหญ่เกินกว่า GPU เดียวจะรองรับได้ ผมจะอธิบายการ setup ด้วย vLLM ซึ่งเป็น engine ที่ optimize สำหรับ LLM inference โดยเฉพาะ
import subprocess
import os
from typing import List, Optional

class DeepSeekProductionDeployer:
    """
    Production deployment setup for DeepSeek R1 671B
    Supports multi-GPU tensor parallelism and pipeline parallelism
    """
    
    def __init__(
        self,
        model_path: str,
        tensor_parallel_size: int = 4,
        pipeline_parallel_size: int = 1,
        gpu_memory_utilization: float = 0.92
    ):
        self.model_path = model_path
        self.tensor_parallel_size = tensor_parallel_size
        self.pipeline_parallel_size = pipeline_parallel_size
        self.gpu_memory_utilization = gpu_memory_utilization
        self.process = None
        
    def start_vllm_server(
        self,
        port: int = 8000,
        host: str = "0.0.0.0",
        max_model_len: int = 32768,
        enforce_eager: bool = False,
        enable_prefix_caching: bool = True
    ):
        """
        Start vLLM server for DeepSeek R1 671B
        Optimized for multi-GPU deployment
        """
        
        cmd = [
            "python", "-m", "vllm.entrypoints.openai.api_server",
            "--model", self.model_path,
            "--dtype", "half",  # Use FP16 for balance of speed and accuracy
            "--tensor-parallel-size", str(self.tensor_parallel_size),
            "--pipeline-parallel-size", str(self.pipeline_parallel_size),
            "--gpu-memory-utilization", str(self.gpu_memory_utilization),
            "--max-model-len", str(max_model_len),
            "--port", str(port),
            "--host", host,
            "--enforce-eager", str(enforce_eager).lower(),
            "--enable-prefix-caching",
            "--disable-log-requests",
            "--save-metrics"
        ]
        
        # Add DeepSeek-specific settings
        env = os.environ.copy()
        env["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
        env["CUDA_VISIBLE_DEVICES"] = "0,1,2,3"  # 4x A100 80GB
        
        print("Starting vLLM server with command:")
        print(" ".join(cmd))
        
        self.process = subprocess.Popen(
            cmd,
            env=env,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT
        )
        
        # Wait for server to be ready
        import time
        import requests
        for _ in range(60):  # Wait up to 60 seconds
            try:
                response = requests.get(f"http://{host}:{port}/health")
                if response.status_code == 200:
                    print("vLLM server is ready!")
                    return True
            except:
                pass
            time.sleep(1)
            
        print("Warning: Server health check timed out, but process may still be starting")
        return True
    
    def get_model_info(self, host: str = "localhost", port: int = 8000):
        """
        Get model information from running server
        """
        import requests
        try:
            response = requests.get(f"http://{host}:{port}/v1/models")
            return response.json()
        except Exception as e:
            return {"error": str(e)}
    
    def stream_chat_completion(
        self,
        messages: List[dict],
        model: str = "deepseek-ai/DeepSeek-R1-671B",
        temperature: float = 0.6,
        max_tokens: int = 2048,
        host: str = "localhost",
        port: int = 8000
    ):
        """
        Streaming chat completion via vLLM API
        """
        import requests
        import json
        
        payload = {
            "model": model,
            "messages": messages,
            "temperature": temperature,
            "max_tokens": max_tokens,
            "stream": True
        }
        
        url = f"http://{host}:{port}/v1/chat/completions"
        
        response = requests.post(
            url,
            json=payload,
            stream=True,
            timeout=300
        )
        
        for line in response.iter_lines():
            if line:
                line = line.decode("utf-8")
                if line.startswith("data: "):
                    data = line[6:]
                    if data != "[DONE]":
                        yield json.loads(data)
    
    def benchmark_throughput(
        self,
        num_requests: int = 100,
        concurrent_users: int = 10,
        host: str = "localhost",
        port: int = 8000
    ):
        """
        Benchmark server throughput with concurrent requests
        """
        import asyncio