{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Google ColabでQwen3.6-27B GGUF + MTPを比較する\n",
    "\n",
    "このNotebookは、Google ColabへGPUが割り当てられた時だけ、Q3_K_Sの正確なGGUFファイルを取得し、llama.cppのbaselineとMTPを同じ条件で比較する実行テンプレートです。ColabのGPU種類・利用時間・VM寿命、速度、OOMの有無は保証されません。機密資料・個人情報を入力せず、公開トンネルや常設serverは作りません。\n",
    "\n",
    "配布時点では実行済み出力を空にしています。`/content/qwen36-mtp-results.json`、`.csv`、`.md`と各runログは、読者のruntimeでだけ生成されます。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import csv\n",
    "import hashlib\n",
    "import json\n",
    "import os\n",
    "import re\n",
    "import shutil\n",
    "import subprocess\n",
    "import sys\n",
    "import threading\n",
    "import time\n",
    "import urllib.request\n",
    "import uuid\n",
    "from datetime import datetime, timezone\n",
    "from pathlib import Path\n",
    "\n",
    "CONTENT = Path('/content')\n",
    "RESULT_JSON = CONTENT / 'qwen36-mtp-results.json'\n",
    "RESULT_CSV = CONTENT / 'qwen36-mtp-results.csv'\n",
    "RESULT_MD = CONTENT / 'qwen36-mtp-results.md'\n",
    "LOG_DIR = CONTENT / 'qwen36-mtp-runs'\n",
    "LOG_DIR.mkdir(exist_ok=True)\n",
    "\n",
    "def run_capture(args, check=False):\n",
    "    result = subprocess.run(args, capture_output=True, text=True, check=check)\n",
    "    return result.returncode, result.stdout.strip(), result.stderr.strip()\n",
    "\n",
    "gpu_rc, gpu_text, gpu_err = run_capture([\n",
    "    'nvidia-smi', '--query-gpu=name,memory.total,memory.free,driver_version',\n",
    "    '--format=csv,noheader,nounits',\n",
    "])\n",
    "if gpu_rc != 0 or not gpu_text:\n",
    "    raise RuntimeError('GPUが割り当てられていません。後続セルを実行しません。nvidia-smi: ' + gpu_err)\n",
    "\n",
    "gpu_name, gpu_total, gpu_free, driver_version = [part.strip() for part in gpu_text.splitlines()[0].split(',')]\n",
    "GPU = {\n",
    "    'name': gpu_name,\n",
    "    'total_mib': int(float(gpu_total)),\n",
    "    'free_mib': int(float(gpu_free)),\n",
    "    'driver_version': driver_version,\n",
    "}\n",
    "print(json.dumps(GPU, ensure_ascii=False, indent=2))\n",
    "print('GPU判定:', 'T4' if 'T4' in GPU['name'] else 'T4以外（実際のGPU名を結果に記録）')\n",
    "\n",
    "for command in [['free', '-h'], ['df', '-h', '/content'], [sys.executable, '--version'], ['cmake', '--version']]:\n",
    "    print('$', ' '.join(command))\n",
    "    rc, stdout, stderr = run_capture(command)\n",
    "    print(stdout or stderr)\n",
    "\n",
    "PREFLIGHT = {\n",
    "    'captured_at': datetime.now(timezone.utc).isoformat(),\n",
    "    'gpu': GPU,\n",
    "    'python': sys.version,\n",
    "    'content_disk_free_bytes': shutil.disk_usage(CONTENT).free,\n",
    "}\n",
    "(CONTENT / 'qwen36-preflight.json').write_text(json.dumps(PREFLIGHT, ensure_ascii=False, indent=2), encoding='utf-8')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# LLAMA_CPP_REFがmasterの間は、検証済み固定commitではなく実行時のcommit依存です。\n",
    "LLAMA_CPP_REF = 'master'\n",
    "LLAMA_CPP_REF_NOTE = 'このNotebookでは固定済み検証commitを主張せず、実行時にcommitを記録します。'\n",
    "LLAMA_CPP_DIR = CONTENT / 'llama.cpp'\n",
    "BUILD_DIR = LLAMA_CPP_DIR / 'build'\n",
    "CLI = BUILD_DIR / 'bin' / 'llama-cli'\n",
    "\n",
    "if not LLAMA_CPP_DIR.exists():\n",
    "    subprocess.run(['git', 'clone', '--depth', '1', 'https://github.com/ggml-org/llama.cpp', str(LLAMA_CPP_DIR)], check=True)\n",
    "subprocess.run(['git', '-C', str(LLAMA_CPP_DIR), 'fetch', '--depth', '1', 'origin', LLAMA_CPP_REF], check=True)\n",
    "# Equivalent shell form for manual reproduction: git checkout \"$LLAMA_CPP_REF\"\n",
    "subprocess.run(['git', '-C', str(LLAMA_CPP_DIR), 'checkout', LLAMA_CPP_REF], check=True)\n",
    "\n",
    "subprocess.run(['cmake', '-S', str(LLAMA_CPP_DIR), '-B', str(BUILD_DIR), '-DGGML_CUDA=ON', '-DBUILD_SHARED_LIBS=OFF', '-DCMAKE_BUILD_TYPE=Release'], check=True)\n",
    "subprocess.run(['cmake', '--build', str(BUILD_DIR), '--config', 'Release', '--target', 'llama-cli', '--parallel', '2'], check=True)\n",
    "if not CLI.exists():\n",
    "    raise RuntimeError('llama-cliのbuild成果物が見つかりません: ' + str(CLI))\n",
    "\n",
    "LLAMA_CPP_COMMIT = subprocess.check_output(['git', '-C', str(LLAMA_CPP_DIR), 'rev-parse', 'HEAD'], text=True).strip()\n",
    "LLAMA_HELP = subprocess.check_output([str(CLI), '--help'], text=True, stderr=subprocess.STDOUT)\n",
    "LLAMA_VERSION = subprocess.check_output([str(CLI), '--version'], text=True, stderr=subprocess.STDOUT)\n",
    "(CONTENT / 'qwen36-llama-cli-help.txt').write_text(LLAMA_HELP, encoding='utf-8')\n",
    "(CONTENT / 'qwen36-llama-cli-version.txt').write_text(LLAMA_VERSION, encoding='utf-8')\n",
    "for required_option in ['draft-mtp', 'spec-draft-n-max']:\n",
    "    if required_option not in LLAMA_HELP:\n",
    "        raise RuntimeError('このcommitのllama-cliが必要なoptionに対応していません: ' + required_option)\n",
    "print('llama.cpp commit:', LLAMA_CPP_COMMIT)\n",
    "print('llama.cpp ref:', LLAMA_CPP_REF)\n",
    "print(LLAMA_VERSION.strip())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "MODEL_REPO = 'unsloth/Qwen3.6-27B-MTP-GGUF'\n",
    "MODEL_QUANT = 'Q3_K_S'\n",
    "MODEL_REVISION = 'main'\n",
    "MODEL_API = f'https://huggingface.co/api/models/{MODEL_REPO}?revision={MODEL_REVISION}'\n",
    "\n",
    "with urllib.request.urlopen(MODEL_API, timeout=60) as response:\n",
    "    MODEL_METADATA = json.load(response)\n",
    "MODEL_REVISION_SHA = MODEL_METADATA.get('sha')\n",
    "if not MODEL_REVISION_SHA:\n",
    "    raise RuntimeError('Hugging Face APIからrevision SHAを取得できませんでした。')\n",
    "\n",
    "candidates = [\n",
    "    item for item in MODEL_METADATA.get('siblings', [])\n",
    "    if item.get('rfilename', '').lower().endswith('.gguf') and MODEL_QUANT.lower() in item.get('rfilename', '').lower()\n",
    "]\n",
    "if len(candidates) != 1:\n",
    "    raise RuntimeError('quantに一致するGGUFを一つに特定できません: ' + repr([item.get('rfilename') for item in candidates]))\n",
    "MODEL_FILE = candidates[0]['rfilename']\n",
    "EXPECTED_SIZE_BYTES = candidates[0].get('size')\n",
    "if not EXPECTED_SIZE_BYTES:\n",
    "    raise RuntimeError('GGUFの期待ファイルサイズをHugging Face APIから取得できませんでした。')\n",
    "MODEL_PATH = CONTENT / 'models' / MODEL_FILE\n",
    "MODEL_PATH.parent.mkdir(parents=True, exist_ok=True)\n",
    "disk_free_before = shutil.disk_usage(CONTENT).free\n",
    "disk_reserve = 4 * 1024**3\n",
    "if disk_free_before < EXPECTED_SIZE_BYTES + disk_reserve:\n",
    "    raise RuntimeError(f'disk不足のためdownloadを止めました。必要目安={EXPECTED_SIZE_BYTES + disk_reserve:,} bytes、空き={disk_free_before:,} bytes')\n",
    "if MODEL_QUANT.startswith('Q4') and GPU['total_mib'] <= 16384:\n",
    "    print('警告: Q4とT4級VRAMの組み合わせです。quantを自動変更せず、VRAMと起動結果を確認します。')\n",
    "if GPU['free_mib'] < 14336:\n",
    "    print('警告: 実行前の空きVRAMが少なめです。context・batch・MTPを自動変更せず記録します。')\n",
    "\n",
    "DOWNLOAD_URL = f'https://huggingface.co/{MODEL_REPO}/resolve/{MODEL_REVISION_SHA}/{MODEL_FILE}'\n",
    "if MODEL_PATH.exists() and MODEL_PATH.stat().st_size == EXPECTED_SIZE_BYTES:\n",
    "    DOWNLOAD_COMPLETE = True\n",
    "    print('既存の完全なGGUFを再利用:', MODEL_PATH)\n",
    "else:\n",
    "    partial_path = MODEL_PATH.with_suffix(MODEL_PATH.suffix + '.part')\n",
    "    with urllib.request.urlopen(DOWNLOAD_URL, timeout=120) as source_file, partial_path.open('wb') as target_file:\n",
    "        shutil.copyfileobj(source_file, target_file, length=1024 * 1024)\n",
    "    if partial_path.stat().st_size != EXPECTED_SIZE_BYTES:\n",
    "        raise RuntimeError(f'GGUF downloadが完了していません。期待={EXPECTED_SIZE_BYTES:,} bytes、実体={partial_path.stat().st_size:,} bytes')\n",
    "    partial_path.replace(MODEL_PATH)\n",
    "    DOWNLOAD_COMPLETE = True\n",
    "\n",
    "MODEL_SHA256 = hashlib.sha256()\n",
    "with MODEL_PATH.open('rb') as model_stream:\n",
    "    for chunk in iter(lambda: model_stream.read(1024 * 1024), b''):\n",
    "        MODEL_SHA256.update(chunk)\n",
    "MODEL_SHA256 = MODEL_SHA256.hexdigest()\n",
    "MODEL_RECORD = {\n",
    "    'repo': MODEL_REPO, 'revision_requested': MODEL_REVISION, 'revision_sha': MODEL_REVISION_SHA,\n",
    "    'file': MODEL_FILE, 'quant': MODEL_QUANT, 'expected_size_bytes': EXPECTED_SIZE_BYTES,\n",
    "    'local_path': str(MODEL_PATH), 'local_size_bytes': MODEL_PATH.stat().st_size,\n",
    "    'sha256': MODEL_SHA256, 'download_complete': DOWNLOAD_COMPLETE, 'download_url': DOWNLOAD_URL,\n",
    "}\n",
    "if MODEL_RECORD['local_size_bytes'] != EXPECTED_SIZE_BYTES:\n",
    "    raise RuntimeError('local GGUFのサイズ検証に失敗しました。')\n",
    "(CONTENT / 'qwen36-model-record.json').write_text(json.dumps(MODEL_RECORD, ensure_ascii=False, indent=2), encoding='utf-8')\n",
    "print(json.dumps({key: MODEL_RECORD[key] for key in ['repo', 'revision_sha', 'file', 'quant', 'expected_size_bytes', 'local_size_bytes', 'download_complete']}, ensure_ascii=False, indent=2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "CONTEXT = 4096\n",
    "OUTPUT_TOKENS = 128\n",
    "PROMPT = '次の文章を、条件を落とさず日本語で3点に要約してください。無料のGoogle Colab GPUは割り当てや利用時間が保証されず、実行結果はGPU・量子化・context・ビルドに依存します。'\n",
    "SAMPLING = {'seed': 42, 'temperature': 0.7, 'top_p': 0.8, 'top_k': 20, 'min_p': 0.0, 'batch_size': 256, 'ubatch_size': 128, 'flash_attn': 'on', 'ngl': 99}\n",
    "RUN_NMAX4 = False  # baseline / n-max=1 / n-max=2が成功し、VRAM余白を確認した後だけTrueにする\n",
    "\n",
    "def common_args():\n",
    "    return [\n",
    "        str(CLI), '-m', str(MODEL_PATH), '--no-mmproj', '--ngl', str(SAMPLING['ngl']),\n",
    "        '--ctx-size', str(CONTEXT), '--n-predict', str(OUTPUT_TOKENS),\n",
    "        '--batch-size', str(SAMPLING['batch_size']), '--ubatch-size', str(SAMPLING['ubatch_size']),\n",
    "        '--flash-attn', SAMPLING['flash_attn'], '--seed', str(SAMPLING['seed']),\n",
    "        '--temp', str(SAMPLING['temperature']), '--top-p', str(SAMPLING['top_p']),\n",
    "        '--top-k', str(SAMPLING['top_k']), '--min-p', str(SAMPLING['min_p']),\n",
    "        '--single-turn', '--prompt', PROMPT,\n",
    "    ]\n",
    "\n",
    "def command_for(condition, n_max=None):\n",
    "    args = common_args()\n",
    "    if condition == 'baseline':\n",
    "        return args + ['--spec-type', 'none']\n",
    "    if condition == 'mtp':\n",
    "        return args + ['--spec-type', 'draft-mtp', '--spec-draft-n-max', str(n_max)]\n",
    "    raise ValueError(condition)\n",
    "\n",
    "PROMPT_SHA256 = hashlib.sha256(PROMPT.encode('utf-8')).hexdigest()\n",
    "print('exact GGUF:', MODEL_FILE)\n",
    "print('context:', CONTEXT, 'output tokens:', OUTPUT_TOKENS, 'prompt sha256:', PROMPT_SHA256)\n",
    "print('execution matrix: baseline, MTP n-max=1, MTP n-max=2; optional n-max=4:', RUN_NMAX4)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def gpu_snapshot():\n",
    "    rc, stdout, _ = run_capture(['nvidia-smi', '--query-gpu=name,memory.total,memory.free,memory.used', '--format=csv,noheader,nounits'])\n",
    "    if rc != 0 or not stdout:\n",
    "        return None\n",
    "    name, total, free, used = [part.strip() for part in stdout.splitlines()[0].split(',')]\n",
    "    return {'name': name, 'total_mib': int(float(total)), 'free_mib': int(float(free)), 'used_mib': int(float(used))}\n",
    "\n",
    "def parse_metrics(text):\n",
    "    def number(patterns):\n",
    "        for pattern in patterns:\n",
    "            match = re.search(pattern, text, re.IGNORECASE)\n",
    "            if match:\n",
    "                return float(match.group(1).replace(',', ''))\n",
    "        return None\n",
    "    prompt_tokens = number([r'prompt eval time\\s*=\\s*[^/]+/\\s*(\\d+)\\s+tokens?', r'prompt[^\\n]*?(\\d+)\\s+tokens?'])\n",
    "    generated_tokens = number([r'(?:^|\\n)eval time\\s*=\\s*[^/]+/\\s*(\\d+)\\s+tokens?', r'(?:generated|generation|decode)[^\\n]*?(\\d+)\\s+tokens?'])\n",
    "    prompt_tps = number([r'prompt eval time[^\\n]*?\\(([\\d.]+)\\s+tokens?\\s+per\\s+second\\)', r'prompt[^\\n]*?([\\d.]+)\\s+tokens?\\s*(?:per|/)\\s*second'])\n",
    "    generation_tps = number([r'(?:^|\\n)eval time[^\\n]*?\\(([\\d.]+)\\s+tokens?\\s+per\\s+second\\)', r'(?:generation|decode|eval)[^\\n]*?([\\d.]+)\\s+tokens?\\s*(?:per|/)\\s*second'])\n",
    "    accepted = number([r'accepted[^\\n]*?(\\d+)\\s+tokens?', r'accepted\\s+tokens?\\s*[:=]\\s*(\\d+)'])\n",
    "    drafted = number([r'draft(?:ed)?[^\\n]*?(\\d+)\\s+tokens?', r'draft(?:ed)?\\s+tokens?\\s*[:=]\\s*(\\d+)'])\n",
    "    return {\n",
    "        'prompt_tokens': int(prompt_tokens) if prompt_tokens is not None else None,\n",
    "        'generated_tokens': int(generated_tokens) if generated_tokens is not None else None,\n",
    "        'prompt_tokens_per_second': prompt_tps, 'generation_tokens_per_second': generation_tps,\n",
    "        'accepted_tokens': int(accepted) if accepted is not None else None,\n",
    "        'drafted_tokens': int(drafted) if drafted is not None else None,\n",
    "    }\n",
    "\n",
    "def classify_error(exit_code, text):\n",
    "    if exit_code == 0: return None\n",
    "    lowered = text.lower()\n",
    "    if re.search(r'out of memory|cuda error|cublas.*alloc|oom', lowered): return 'oom'\n",
    "    if re.search(r'unknown option|unrecognized option|invalid argument|unknown argument', lowered): return 'unsupported_option'\n",
    "    if re.search(r'could not.*(download|open)|http error|connection.*(reset|refused)|hugging face', lowered): return 'download_error'\n",
    "    if re.search(r'cmake|ninja|compilation|build failed', lowered): return 'build_error'\n",
    "    if re.search(r'no such file|not found', lowered): return 'missing_file_or_binary'\n",
    "    return 'runtime_error'\n",
    "\n",
    "def run_condition(condition, n_max=None, timeout_seconds=1800):\n",
    "    run_id = '{}-{}-{}-{}'.format(condition, n_max or 'baseline', datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ'), uuid.uuid4().hex[:8])\n",
    "    stdout_path, stderr_path = LOG_DIR / f'{run_id}.stdout.log', LOG_DIR / f'{run_id}.stderr.log'\n",
    "    started_at = datetime.now(timezone.utc)\n",
    "    before = gpu_snapshot()\n",
    "    peak_used = before['used_mib'] if before else None\n",
    "    minimum_free = before['free_mib'] if before else None\n",
    "    process = None\n",
    "    with stdout_path.open('w', encoding='utf-8') as stdout_file, stderr_path.open('w', encoding='utf-8') as stderr_file:\n",
    "        process = subprocess.Popen(command_for(condition, n_max), stdout=stdout_file, stderr=stderr_file, text=True)\n",
    "        while process.poll() is None:\n",
    "            snapshot = gpu_snapshot()\n",
    "            if snapshot:\n",
    "                peak_used = max(peak_used or snapshot['used_mib'], snapshot['used_mib'])\n",
    "                minimum_free = min(minimum_free if minimum_free is not None else snapshot['free_mib'], snapshot['free_mib'])\n",
    "            if (datetime.now(timezone.utc) - started_at).total_seconds() > timeout_seconds:\n",
    "                process.terminate()\n",
    "                process.wait(timeout=30)\n",
    "                break\n",
    "            time.sleep(1)\n",
    "    ended_at = datetime.now(timezone.utc)\n",
    "    after = gpu_snapshot()\n",
    "    stdout_text = stdout_path.read_text(encoding='utf-8', errors='replace')\n",
    "    stderr_text = stderr_path.read_text(encoding='utf-8', errors='replace')\n",
    "    combined = stdout_text + '\\n' + stderr_text\n",
    "    metrics = parse_metrics(combined)\n",
    "    drafted = metrics['drafted_tokens']\n",
    "    accepted = metrics['accepted_tokens']\n",
    "    metrics['acceptance_rate'] = accepted / drafted if accepted is not None and drafted not in (None, 0) else None\n",
    "    return {\n",
    "        'condition_id': condition if condition == 'baseline' else f'{condition}-nmax-{n_max}',\n",
    "        'run_id': run_id, 'started_at': started_at.isoformat(), 'ended_at': ended_at.isoformat(),\n",
    "        'gpu_name': (before or GPU)['name'], 'gpu_total_mib': (before or GPU)['total_mib'],\n",
    "        'free_vram_before_mib': before['free_mib'] if before else None,\n",
    "        'peak_vram_used_mib': peak_used, 'minimum_free_vram_mib': minimum_free,\n",
    "        'free_vram_after_mib': after['free_mib'] if after else None,\n",
    "        'wall_clock_seconds': round((ended_at - started_at).total_seconds(), 3),\n",
    "        'exit_code': process.returncode if process else None, **metrics,\n",
    "        'context': CONTEXT, 'quant': MODEL_QUANT, 'model_repo': MODEL_REPO,\n",
    "        'model_file': MODEL_FILE, 'model_revision': MODEL_REVISION_SHA,\n",
    "        'llama_cpp_commit': LLAMA_CPP_COMMIT, 'n_max': n_max, 'sampling': SAMPLING,\n",
    "        'prompt_sha256': PROMPT_SHA256, 'stdout_path': str(stdout_path), 'stderr_path': str(stderr_path),\n",
    "        'error_class': classify_error(process.returncode if process else 1, combined),\n",
    "    }"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "results = []\n",
    "baseline = run_condition('baseline')\n",
    "results.append(baseline)\n",
    "if baseline['exit_code'] == 0:\n",
    "    results.append(run_condition('mtp', 1))\n",
    "    results.append(run_condition('mtp', 2))\n",
    "else:\n",
    "    print('baselineが非zeroで終了したため、MTPは実行しません:', baseline['error_class'])\n",
    "\n",
    "if RUN_NMAX4 and len(results) >= 3 and all(item['exit_code'] == 0 for item in results[:3]):\n",
    "    results.append(run_condition('mtp', 4))\n",
    "elif RUN_NMAX4:\n",
    "    print('n-max=4はbaseline / n-max=1 / n-max=2の成功確認後だけ実行します。今回はskipしました。')\n",
    "\n",
    "summary = {\n",
    "    'created_at': datetime.now(timezone.utc).isoformat(),\n",
    "    'preflight': PREFLIGHT, 'model': MODEL_RECORD,\n",
    "    'llama_cpp': {'ref': LLAMA_CPP_REF, 'commit': LLAMA_CPP_COMMIT, 'version': LLAMA_VERSION.strip()},\n",
    "    'matrix': ['baseline', 'mtp-nmax-1', 'mtp-nmax-2'] + (['mtp-nmax-4'] if RUN_NMAX4 else []),\n",
    "    'results': results,\n",
    "    'restart_safe_note': 'run_id付きログ、モデルrecord、結果JSON/CSV/Markdownを/contentに残します。runtime再起動後はセルを上から再実行してください。',\n",
    "}\n",
    "RESULT_JSON.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding='utf-8')\n",
    "\n",
    "fieldnames = sorted({key for result in results for key in result})\n",
    "with RESULT_CSV.open('w', newline='', encoding='utf-8-sig') as csv_file:\n",
    "    writer = csv.DictWriter(csv_file, fieldnames=fieldnames, extrasaction='ignore')\n",
    "    writer.writeheader()\n",
    "    writer.writerows(results)\n",
    "\n",
    "lines = ['# Qwen3.6-27B GGUF + MTP results', '', '- generated: {}'.format(summary['created_at']), '- model: `{}` / `{}` / revision `{}`'.format(MODEL_REPO, MODEL_FILE, MODEL_REVISION_SHA), '- llama.cpp commit: `{}`'.format(LLAMA_CPP_COMMIT), '- GPU: `{}` / total {} MiB'.format(GPU['name'], GPU['total_mib']), '', '| condition | exit | generation tok/s | peak VRAM MiB | accepted / drafted | error |', '|---|---:|---:|---:|---:|---|']\n",
    "for item in results:\n",
    "    accepted_text = '{} / {}'.format(item['accepted_tokens'], item['drafted_tokens'])\n",
    "    lines.append('| {} | {} | {} | {} | {} | {} |'.format(item['condition_id'], item['exit_code'], item['generation_tokens_per_second'], item['peak_vram_used_mib'], accepted_text, item['error_class'] or 'ok'))\n",
    "lines += ['', 'Numbers are runtime measurements for this exact GPU, GGUF file, revision, commit, prompt, and sampling. Null means the log did not expose the metric; it is not zero.']\n",
    "RESULT_MD.write_text('\\n'.join(lines) + '\\n', encoding='utf-8')\n",
    "print('saved:', RESULT_JSON, RESULT_CSV, RESULT_MD)\n",
    "print(json.dumps(results, ensure_ascii=False, indent=2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 実行後に確認すること\n",
    "\n",
    "`qwen36-mtp-results.json`でGPU名・total/free/peak VRAM・wall clock・prompt/generation tokens/s・accepted/drafted・acceptance rate・context・quant・exact file・model revision・llama.cpp commit・n-max・sampling・error classを確認します。ログに数値がない項目はnullのまま扱い、0と解釈しません。\n",
    "\n",
    "baselineが失敗した場合はMTPを実行しません。MTPだけが失敗した場合は、baselineとのVRAM差、unsupported option、OOM、download、disk/RAM、build、partial downloadをログの段階で分類し、設定を自動変更せず再実行条件を記録してください。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
  "language_info": {"name": "python", "version": "3"}
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
