エッジAIモデル最適化実践:量子化・プルーニング・デプロイのフルパイプラインガイド

AI与大数据

サマリー

  • エッジAIは2026年最大の成長市場:世界のエッジAIチップ出荷量は50億個超と予測、モデル最適化が実用化のボトルネック
  • 量子化3段階:FP16→INT8→INT4、INT8量子化の精度劣化は1%未満、INT4はGPTQ/AWQアルゴリズムとの組み合わせが必要
  • 構造化プルーニング2つのアプローチ:マグニチュードプルーニングはシンプルかつ高効率、蒸留プルーニングは精度面で優位、組み合わせでパラメータ70%以上を圧縮可能
  • デプロイ3大フレームワーク:ONNX Runtimeは汎用性最強、TensorRTはパフォーマンス最適、TFLiteはモバイル向け推奨
  • 本記事ではモデル最適化からエッジデプロイまでのフルパイプライン実践コードを提供

目次


エッジAI:AI実用化のラストマイル

エッジAI市場の現状

デバイスタイプ 2026年出荷量 典型的な演算力 典型的な用途
スマートフォン 15億 30-60 TOPS 音声アシスタント、撮影強化
スマートカメラ 5億 2-8 TOPS 顔認識、行動検知
自動車(ADAS) 1億 50-200 TOPS 自動運転、DMS
IoTゲートウェイ 3億 1-4 TOPS 予知保全、異常検知
産業用コントローラ 5000万 5-20 TOPS 品質検査、ロボット制御

エッジAIの主要課題

課題 クラウド エッジ
演算力 1000+ TOPS 2-60 TOPS 50-500×
メモリ 80-640GB 2-16GB 5-80×
消費電力 300W+ 5-30W 10-60×
レイテンシ 50-200ms(ネットワーク) 1-10ms(ローカル) エッジ優位
プライバシー データをクラウドへ ローカル処理 エッジ優位

エッジAIチップの状況

チップ メーカー 演算力 消費電力 エコシステム
Jetson Orin NX NVIDIA 100 TOPS 25W CUDA+TensorRT
Hailo-8 Hailo 26 TOPS 2.5W 専用コンパイラ
Google Edge TPU Google 4 TOPS 2W TFLite
Rockchip RK3588 Rockchip 6 TOPS 10W RKNN
Huawei昇騰310P Huawei 22 TOPS 8W MindSpore
Horizon Robotics J5 Horizon Robotics 128 TOPS 15W 天工開物

モデル量子化3段階

量子化の原理と精度への影響

┌──────────────────────────────────────────────────────────────┐
│              量子化3段階                                         │
│                                                                │
│  FP32 → FP16 (半精度)                                        │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  精度劣化: <0.1%                                     │    │
│  │  容量削減: 50%                                       │    │
│  │  速度向上: 1.5-2×                                    │    │
│  │  手法: 直接変換、キャリブレーション不要                              │    │
│  └──────────────────────────────────────────────────────┘    │
│                         ↓                                     │
│  FP16 → INT8 (8bit整数量子化)                                   │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  精度劣化: 0.5-2%                                    │    │
│  │  容量削減: 75% (vs FP32)                             │    │
│  │  速度向上: 2-4×                                      │    │
│  │  手法: PTQ(訓練後量子化) / QAT(量子化対応訓練)             │    │
│  └──────────────────────────────────────────────────────┘    │
│                         ↓                                     │
│  INT8 → INT4 (4bit整数量子化)                                   │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  精度劣化: 2-5% (GPTQ/AWQによる補償が必要)                     │    │
│  │  容量削減: 87.5% (vs FP32)                           │    │
│  │  速度向上: 3-6×                                      │    │
│  │  手法: GPTQ / AWQ / AQLM / HQQ                      │    │
│  └──────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────┘

INT8 PTQ量子化実践

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from neural_compressor import QuantizationAwareTraining, PostTrainingQuantConfig

def int8_ptq_quantize(model_path, output_path, calibration_data):
    model = AutoModelForCausalLM.from_pretrained(
        model_path,
        torch_dtype=torch.float16,
        device_map="auto",
    )
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    
    config = PostTrainingQuantConfig(
        approach="static",
        tuning_criterion={
            "max_trials": 100,
            "objective": "accuracy",
        },
        quant_format="QDQ",
        calibration_sampling_size=512,
    )
    
    def calibration_dataloader():
        for text in calibration_data:
            inputs = tokenizer(text, return_tensors="pt", max_length=512)
            yield {"input_ids": inputs["input_ids"]}
    
    from neural_compressor import quantize
    q_model = quantize(
        model,
        config,
        calibration_dataloader=calibration_dataloader,
    )
    
    q_model.save(output_path)
    return q_model

def int8_gptq_quantize(model_path, output_path, calibration_data):
    from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
    
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    model = AutoGPTQForCausalLM.from_pretrained(
        model_path,
        torch_dtype=torch.float16,
        device_map="auto",
    )
    
    quantize_config = BaseQuantizeConfig(
        bits=8,
        group_size=128,
        desc_act=True,
        damp_percent=0.01,
        sym=True,
    )
    
    examples = []
    for text in calibration_data:
        tokenized = tokenizer(text, return_tensors="pt", max_length=2048)
        examples.append(tokenized["input_ids"])
    
    model.quantize(examples, quantize_config=quantize_config)
    model.save_quantized(output_path)
    return model

INT4量子化実践(GPTQ/AWQ)

def int4_gptq_quantize(model_path, output_path, calibration_data):
    from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
    
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    model = AutoGPTQForCausalLM.from_pretrained(
        model_path,
        torch_dtype=torch.float16,
        device_map="auto",
    )
    
    quantize_config = BaseQuantizeConfig(
        bits=4,
        group_size=128,
        desc_act=True,
        damp_percent=0.01,
        sym=False,
    )
    
    examples = []
    for text in calibration_data[:256]:
        tokenized = tokenizer(text, return_tensors="pt", max_length=2048)
        examples.append(tokenized["input_ids"])
    
    model.quantize(examples, quantize_config=quantize_config)
    model.save_quantized(output_path)
    return model

def int4_awq_quantize(model_path, output_path, calibration_data):
    from awq import AutoAWQForCausalLM
    
    model = AutoAWQForCausalLM.from_pretrained(model_path)
    tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
    
    quant_config = {
        "zero_point": True,
        "q_group_size": 128,
        "w_bit": 4,
        "version": "GEMM",
    }
    
    model.quantize(
        tokenizer,
        quant_config=quant_config,
        calib_data=calibration_data[:256],
    )
    
    model.save_quantized(output_path)
    return model

量子化効果の比較

量子化方式 7Bモデル容量 推論速度 精度劣化 VRAM要件
FP32 28GB ベースライン 32GB
FP16 14GB 1.8× <0.1% 16GB
INT8 PTQ 7GB 3.2× 0.5-2% 8GB
INT8 GPTQ 7GB 3.5× 0.3-1% 8GB
INT4 GPTQ 3.5GB 5.0× 2-4% 4GB
INT4 AWQ 3.5GB 5.5× 1-3% 4GB

構造化プルーニング実践

プルーニング手法の比較

手法 粒度 精度劣化 ハードウェア適合性 複雑度
非構造化プルーニング 個別重み 低い(疎計算)
構造化プルーニング チャネル/レイヤ 高い(密計算)
蒸留プルーニング チャネル+蒸留 高い
自動プルーニング(NAS) 探索空間 高い 極めて高い

マグニチュードプルーニング実践

import torch
import torch.nn as nn
from transformers import AutoModelForCausalLM

class MagnitudePruner:
    def __init__(self, model, sparsity_ratio=0.5):
        self.model = model
        self.sparsity_ratio = sparsity_ratio
        self.masks = {}
    
    def compute_masks(self):
        for name, module in self.model.named_modules():
            if isinstance(module, nn.Linear):
                weight = module.weight.data.abs()
                threshold = torch.quantile(
                    weight.flatten(), self.sparsity_ratio
                )
                self.masks[name] = (weight > threshold).float()
    
    def apply_masks(self):
        for name, module in self.model.named_modules():
            if name in self.masks and isinstance(module, nn.Linear):
                module.weight.data *= self.masks[name]
    
    def structured_prune(self):
        for name, module in self.model.named_modules():
            if isinstance(module, nn.Linear):
                weight = module.weight.data
                row_norms = weight.norm(dim=1)
                threshold = torch.quantile(
                    row_norms, self.sparsity_ratio
                )
                keep_mask = (row_norms > threshold).float()
                module.weight.data *= keep_mask.unsqueeze(1)
                if module.bias is not None:
                    module.bias.data *= keep_mask
    
    def fine_tune(self, dataloader, epochs=3, lr=1e-5):
        optimizer = torch.optim.AdamW(self.model.parameters(), lr=lr)
        
        for epoch in range(epochs):
            for batch in dataloader:
                outputs = self.model(**batch)
                loss = outputs.loss
                loss.backward()
                optimizer.step()
                optimizer.zero_grad()
                self.apply_masks()
        
        return self.model

def prune_model(model_path, output_path, sparsity=0.5):
    model = AutoModelForCausalLM.from_pretrained(
        model_path, torch_dtype=torch.float16, device_map="auto"
    )
    
    pruner = MagnitudePruner(model, sparsity_ratio=sparsity)
    pruner.structured_prune()
    
    model.save_pretrained(output_path)
    return model

プルーニング+蒸留の組み合わせ

class PruneDistillPipeline:
    def __init__(self, teacher_path, student_path, sparsity=0.5):
        self.teacher = AutoModelForCausalLM.from_pretrained(
            teacher_path, torch_dtype=torch.float16, device_map="auto"
        )
        self.student = AutoModelForCausalLM.from_pretrained(
            student_path, torch_dtype=torch.float16, device_map="auto"
        )
        self.pruner = MagnitudePruner(self.student, sparsity)
    
    def train(self, dataloader, epochs=5):
        optimizer = torch.optim.AdamW(self.student.parameters(), lr=2e-5)
        kl_loss = nn.KLDivLoss(reduction="batchmean")
        
        for epoch in range(epochs):
            for batch in dataloader:
                with torch.no_grad():
                    teacher_out = self.teacher(**batch)
                
                student_out = self.student(**batch)
                
                hard_loss = student_out.loss
                
                soft_loss = kl_loss(
                    F.log_softmax(student_out.logits / 2.0, dim=-1),
                    F.softmax(teacher_out.logits / 2.0, dim=-1),
                ) * 4.0
                
                total_loss = 0.3 * hard_loss + 0.7 * soft_loss
                
                total_loss.backward()
                optimizer.step()
                optimizer.zero_grad()
                self.pruner.apply_masks()
        
        return self.student

プルーニング効果の比較

プルーニング方式 パラメータ保持率 精度劣化 推論高速化 容量削減
マグニチュードプルーニング50% 50% 3-5% 1.8× 50%
マグニチュードプルーニング70% 30% 8-12% 2.5× 70%
蒸留プルーニング50% 50% 1-3% 1.8× 50%
蒸留プルーニング70% 30% 4-6% 2.5× 70%
プルーニング+量子化 30%+INT8 5-8% 85%

ONNX Runtimeデプロイ実践

PyTorch→ONNXエクスポート

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

def export_to_onnx(model_path, output_path, opset=17):
    model = AutoModelForCausalLM.from_pretrained(
        model_path, torch_dtype=torch.float16
    )
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    model.eval()
    
    dummy_input = tokenizer("Hello", return_tensors="pt")
    input_ids = dummy_input["input_ids"]
    attention_mask = dummy_input["attention_mask"]
    
    torch.onnx.export(
        model,
        (input_ids, attention_mask),
        output_path,
        opset_version=opset,
        input_names=["input_ids", "attention_mask"],
        output_names=["logits"],
        dynamic_axes={
            "input_ids": {0: "batch", 1: "seq_len"},
            "attention_mask": {0: "batch", 1: "seq_len"},
            "logits": {0: "batch", 1: "seq_len"},
        },
        do_constant_folding=True,
    )
    print(f"Exported to {output_path}")

ONNX Runtime推論

import onnxruntime as ort
import numpy as np

class ONNXInferenceEngine:
    def __init__(self, model_path, provider="CUDAExecutionProvider"):
        sess_options = ort.SessionOptions()
        sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
        sess_options.intra_op_num_threads = 4
        sess_options.inter_op_num_threads = 4
        
        self.session = ort.InferenceSession(
            model_path,
            sess_options=sess_options,
            providers=[provider],
        )
        
        self.io_binding = self.session.io_binding()
    
    def infer(self, input_ids, attention_mask):
        input_ids_np = np.array(input_ids, dtype=np.int64)
        attention_mask_np = np.array(attention_mask, dtype=np.int64)
        
        outputs = self.session.run(
            ["logits"],
            {
                "input_ids": input_ids_np,
                "attention_mask": attention_mask_np,
            },
        )
        
        return outputs[0]
    
    def infer_with_io_binding(self, input_ids, attention_mask):
        input_ids_tensor = ort.OrtValue.ortvalue_from_numpy(
            np.array(input_ids, dtype=np.int64), "cuda", 0
        )
        attention_mask_tensor = ort.OrtValue.ortvalue_from_numpy(
            np.array(attention_mask, dtype=np.int64), "cuda", 0
        )
        
        self.io_binding.bind_ortvalue_input("input_ids", input_ids_tensor)
        self.io_binding.bind_ortvalue_input("attention_mask", attention_mask_tensor)
        self.io_binding.bind_output("logits", "cuda", 0)
        
        self.session.run_with_iobinding(self.io_binding)
        
        return self.io_binding.get_outputs()[0].numpy()

ONNX Runtimeパフォーマンス比較

設定 レイテンシ(ms) スループット(req/s) VRAM
PyTorch FP16 45 22 8GB
ORT FP16 32 31 6GB
ORT INT8 18 55 4GB
ORT INT8+IO Binding 14 71 4GB
ORT INT8+Batch=8 8/batch 125 6GB

TensorRT最適化実践

ONNX→TensorRT変換

import tensorrt as trt

def build_tensorrt_engine(
    onnx_path,
    engine_path,
    fp16=True,
    int8=False,
    max_batch_size=8,
    max_workspace=4 << 30,
):
    logger = trt.Logger(trt.Logger.WARNING)
    builder = trt.Builder(logger)
    network = builder.create_network(
        1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
    )
    parser = trt.OnnxParser(network, logger)
    
    with open(onnx_path, "rb") as f:
        if not parser.parse(f.read()):
            for i in range(parser.num_errors):
                print(f"Parse error: {parser.get_error(i)}")
            return None
    
    config = builder.create_builder_config()
    config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, max_workspace)
    
    if fp16:
        config.set_flag(trt.BuilderFlag.FP16)
    
    if int8:
        config.set_flag(trt.BuilderFlag.INT8)
        config.int8_calibrator = None
    
    profile = builder.create_optimization_profile()
    profile.set_shape(
        "input_ids",
        min=(1, 1),
        opt=(1, 128),
        max=(max_batch_size, 2048),
    )
    profile.set_shape(
        "attention_mask",
        min=(1, 1),
        opt=(1, 128),
        max=(max_batch_size, 2048),
    )
    config.add_optimization_profile(profile)
    
    engine = builder.build_serialized_network(network, config)
    
    with open(engine_path, "wb") as f:
        f.write(engine)
    
    print(f"TensorRT engine saved to {engine_path}")
    return engine

class TensorRTInference:
    def __init__(self, engine_path):
        self.logger = trt.Logger(trt.Logger.WARNING)
        self.runtime = trt.Runtime(self.logger)
        
        with open(engine_path, "rb") as f:
            self.engine = self.runtime.deserialize_cuda_engine(f.read())
        
        self.context = self.engine.create_execution_context()
    
    def infer(self, input_ids, attention_mask):
        import pycuda.driver as cuda
        import pycuda.autoinit
        
        input_ids_np = np.array(input_ids, dtype=np.int32)
        attention_mask_np = np.array(attention_mask, dtype=np.int32)
        
        d_input_ids = cuda.mem_alloc(input_ids_np.nbytes)
        d_attention_mask = cuda.mem_alloc(attention_mask_np.nbytes)
        
        cuda.memcpy_htod(d_input_ids, input_ids_np)
        cuda.memcpy_htod(d_attention_mask, attention_mask_np)
        
        output_shape = self.engine.get_binding_shape(2)
        d_output = cuda.mem_alloc(
            trt.volume(output_shape) * np.dtype(np.float32).itemsize
        )
        
        bindings = [int(d_input_ids), int(d_attention_mask), int(d_output)]
        
        self.context.execute_v2(bindings)
        
        output = np.empty(output_shape, dtype=np.float32)
        cuda.memcpy_dtoh(output, d_output)
        
        return output

TensorRTパフォーマンス比較

設定 レイテンシ(ms) スループット(req/s) 高速化比
PyTorch FP16 45 22
ORT INT8 18 55 2.5×
TRT FP16 15 66
TRT INT8 8 125 5.6×
TRT INT8+Batch=8 3/batch 333 15×

エッジデバイス推論戦略

階層型推論アーキテクチャ

┌──────────────────────────────────────────────────────────────┐
│              エッジ・クラウド協調推論アーキテクチャ                                │
│                                                                │
│  クラウド(大規模モデル)                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  70Bモデル → 複雑な推論、長文生成、マルチターン対話              │    │
│  │  レイテンシ: 200-500ms | コスト: 高                           │    │
│  └──────────────────────────────────────────────────────┘    │
│                         ↕ ネットワーク                                │
│  エッジゲートウェイ(中規模モデル)                                           │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  7B量子化モデル → 日常的な推論、簡単な対話、意図認識              │    │
│  │  レイテンシ: 20-50ms | コスト: 中                             │    │
│  └──────────────────────────────────────────────────────┘    │
│                         ↕ ローカルバス                            │
│  エンドデバイス(小規模モデル)                                             │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  プルーニング+量子化小規模モデル → キーワード検出、ウェイクワード、簡単な分類         │    │
│  │  レイテンシ: 1-5ms | コスト: なし                               │    │
│  └──────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────┘

エッジデプロイコード(Jetson)

import jetson.inference
import jetson.utils

class EdgeInferencePipeline:
    def __init__(self, model_type="classification", threshold=0.7):
        self.model = jetson.inference.initialize(model_type)
        self.threshold = threshold
        self.fallback_enabled = True
    
    def infer_local(self, image):
        try:
            results = self.model.Detect(image)
            confident = [r for r in results if r.Confidence >= self.threshold]
            
            if not confident and self.fallback_enabled:
                return self.infer_cloud(image)
            
            return confident
        except Exception as e:
            if self.fallback_enabled:
                return self.infer_cloud(image)
            raise
    
    def infer_cloud(self, image):
        import requests
        _, compressed = cv2.imencode(".jpg", image, [cv2.IMWRITE_JPEG_QUALITY, 85])
        
        response = requests.post(
            "https://api.example.com/infer",
            files={"image": compressed.tobytes()},
            timeout=5.0,
        )
        return response.json()
    
    def benchmark(self, image, iterations=100):
        import time
        
        latencies = []
        for _ in range(iterations):
            start = time.perf_counter()
            self.infer_local(image)
            latencies.append((time.perf_counter() - start) * 1000)
        
        return {
            "mean_ms": sum(latencies) / len(latencies),
            "p50_ms": sorted(latencies)[len(latencies) // 2],
            "p99_ms": sorted(latencies)[int(len(latencies) * 0.99)],
        }

エッジデバイスパフォーマンス実測

デバイス モデル 量子化 レイテンシ(ms) 精度
Jetson Orin NX BERT-base FP16 12 99.2%
Jetson Orin NX BERT-base INT8 6 98.8%
Jetson Orin NX ResNet50 FP16 8 99.5%
Jetson Orin NX ResNet50 INT8 4 99.1%
RK3588 MobileBERT INT8 25 97.5%
RK3588 MobileNetV3 INT8 8 98.2%
Raspberry Pi 5 TinyBERT INT8 120 95.8%
Raspberry Pi 5 MobileNetV2 INT8 45 97.0%

まとめ

重要ポイントの振り返り

  1. 量子化:INT8 PTQがコストパフォーマンス最適、INT4はGPTQ/AWQによる精度補償が必要
  2. プルーニング:構造化プルーニング+蒸留の組み合わせでパラメータ70%以上を圧縮、精度劣化は制御可能
  3. デプロイ:ONNX Runtimeは汎用的、TensorRTは極限のパフォーマンス、TFLiteはモバイル向け
  4. エッジ戦略:階層型推論+クラウドフォールバックでレイテンシと精度を両立

エッジAI最適化ロードマップ

フェーズ 最適化手法 期待効果
ステップ1 FP16エクスポート+ONNX Runtime 2×高速化
ステップ2 INT8量子化 4×高速化
ステップ3 構造化プルーニング 容量-50%
ステップ4 TensorRT最適化 6×高速化
ステップ5 エッジ・クラウド協調 柔軟なデプロイ

AI生成画像を圧縮する必要がありますか?画像圧縮ツール動画圧縮をお試しください。エッジデプロイの素材を素早く最適化できます。

関連記事

ブラウザローカルツールを無料で試す →

#边缘AI优化#模型量化#ONNX Runtime#TensorRT部署#边缘推理优化#2026