Wake-on-LAN実装でComfyUIを自動起動する全記録

プログラミング

背景:処理時間と消費電力のバランス

ブログ記事のアイキャッチ画像をFLUX.1 schnellで生成する体制を構築しました。当初はMacBook Pro M1 Pro(32GB)で実行していましたが、1記事あたり約35分という所要時間はワークフローとして実用的ではありません。そこで、RTX 3070(8GB VRAM)を搭載したWindowsデスクトップ機にComfyUIをセットアップし、LAN経由で遠隔操作する構成へ切り替えました。その結果、生成時間は1枚あたり3〜4分程度まで劇的に短縮されています。

しかし、ここで新たな悩みが浮上します。画像生成は1日に数回という限定的な用途であるのに対し、Windows機を常時稼働させるのは電気代と騒音の観点から合理的ではありません。本環境における実測では、ComfyUIがアイドル状態(GPU初期化済み)でも約30Wの電力を常時消費し続けていました。

「必要な時だけ起動し、終われば眠らせる」——この当たり前を実現するのがWake-on-LAN(WoL)です。単なるマジックパケットの送信に留まらず、送信側のコード、受信側のWindows設定、そして応答確認のポーリングまでを網羅した、堅牢な自動起動システムの実装記録を共有します。

Wake-on-LANの本質

ネットワーク機器の接続イメージ

Wake-on-LANは、ATX電源仕様およびACPI(Advanced Configuration and Power Interface)によって業界標準化された、スリープ中(あるいは電源オフ)のマシンをネットワーク経由でリモート復帰させる仕組みです。1996年にAMDとIBMが提唱し、ACPI 2.0以降の標準仕様に組み込まれたこの技術は、四半世紀を超えて現代のインフラでも現役で活躍しています。

仕組みは極めて物理層に近いところで完結しています。OSが介在しないスリープ状態であっても、ネットワークインターフェースカード(NIC)には微弱な電力が供給され続けており、自身のMACアドレスを含んだ「マジックパケット」を監視しています。この挙動の詳細は、Microsoft Learnの「Wake-on-LAN (WoL) Behavior in Windows」でも解説されています。

マジックパケットとRFC 863の慣例

マジックパケットは、先頭の6バイト(0xFFの連続)と、それに続く対象マシンのMACアドレスを16回繰り返した、計102バイトのUDPデータです。

[0xFF × 6] [対象マシンの MAC × 16]

このパケットを送信する際、歴史的にポート9(RFC 863で定義されるDiscardプロトコル)が利用されます。ただし、NICハードウェアはペイロード内のパターンのみを検証するため、実際にはポート番号は任意です。とはいえ、一部のネットワーク機器が特定のポートを遮断する可能性を考慮し、ポート7(Echo)とポート9の両方に送出する実装パターンが堅牢です。

Windowsの設定:現代的な「罠」の回避

WoLは枯れた技術ですが、現代のWindows機で確実に動作させるには、複数の設定階層をクリアしなければなりません。

1. 電源状態とModern Standbyの壁

ACPIの定義による電源状態(S状態)によって、WoLの可否が異なります。

状態 名称 WoL対応 注記
S3 スリープ 対応 powercfg /aで「待機状態 (S3)」を確認
S4 休止状態 対応 同上
S5 シャットダウン 追加設定が必要 BIOSとOS設定の両面

ここで直面しやすい課題が、最近のノートPCやマザーボードに見られる「Modern Standby(S0 Low Power Idle)」です。S3をサポートせずS0ixのみに対応する機種では、NICへの給電が遮断されやすく、WoLが機能しないケースが多々あります。

2. 高速スタートアップとハイバネーションの依存性

S5(シャットダウン状態)からの起動を目指す場合、Windowsの「高速スタートアップ」が最大の障壁となります。この機能は、シャットダウン時にカーネルの状態をディスクに保存(ハイバネーション)することで次回起動を早めますが、副作用としてNICの待機電力をカットしてしまうことがあります。

重要な設計知識として、高速スタートアップはハイバネーション機能に依存しています。 そのため、ハイバネーション自体を無効化すれば、連動して高速スタートアップも無効化されます。

  • コマンドによる無効化(推奨):
    管理者権限のPowerShellで以下を実行すれば、ハイバネーションファイルが削除され、設定が確実に反映されます。

    powercfg /hibernate off
    
  • GUIによる手順:

    1. 「コントロールパネル」を開く
    2. 「電源オプション」を選択
    3. 左側メニューから「電源ボタンの動作を選択」を選択
    4. 「現在利用可能でない設定を変更します」をクリック(管理者昇格が必要)
    5. 「高速スタートアップを有効にする」のチェックを外す
    6. 「変更の保存」をクリック

S3(スリープ)運用であれば、高速スタートアップの設定は考慮不要です。

3. NICドライバの二重設定

デバイスマネージャーにおけるNICのプロパティ設定は、詳細設定タブと電源の管理タブの両方を設定しなければなりません。

詳細設定タブ(ドライバにより項目名が異なる場合あり):

  • Wake on Magic Packet → 有効
  • Wake on Pattern Match → 有効(任意。ARPブロードキャストでも起動する設定)
  • Shutdown Wake-On-Lan → 有効(S5から起動させたい場合)
  • Energy Efficient Ethernet → 無効(省電力機能がNICを完全停止させることがある)

電源の管理タブ:

  • ✓ このデバイスで、コンピューターのスタンバイ状態を解除できるようにする
  • ✓ Magic Packetでのみ、コンピューターのスタンバイ状態を解除できるようにする

これらはAND条件であり、一方の設定漏れが不作動の直接的な原因となります。

ログイン画面での足止め

Magic packetを受け取ってマシンが物理的に起動しても、ユーザーログイン画面のまま停止することがあります。ログインせずに放置すると次の問題が発生する可能性があります。

  • アプリケーションレベルのサービス(ComfyUI Desktopなど)が起動しない
  • Windows Defenderファイアウォール が未ログイン状態で厳しく機能する場合がある
  • 設定によってはpingすら通らないこともある

対策案:

  1. 自動ログイン設定(netplwizコマンドで設定可能)
  2. ComfyUIをサービスとして実行(NSSMなどのツールでラップ)
  3. タスクスケジューラの「システム起動時」トリガ(ログイン完了を待たずに実行)

家庭内LAN限定の利用を前提に、自動ログイン設定を採用しています。物理的アクセス時にログイン状態になるリスクはありますが、家庭内運用に限定すれば許容範囲です。

Node.jsでの実装

ネットワークパケット送信

プロトコルが単純なため、Node.jsのdgram(UDPソケット)を使用すれば約30行のコードで実装できます。

import { createSocket } from "node:dgram";

async function sendMagicPacket(
  macAddress: string,
  broadcastIp = "192.168.1.255",
): Promise<void> {
  // MACアドレスから`:`や`-`を除去してhexバイト列に変換
  const mac = macAddress.replace(/[:-]/g, "").toLowerCase();
  if (mac.length !== 12) {
    throw new Error(`不正なMACアドレス: ${macAddress}`);
  }
  const macBuf = Buffer.from(mac, "hex");

  // 先頭6バイト0xFF + MAC × 16回
  const packet = Buffer.concat([
    Buffer.alloc(6, 0xff),
    ...Array.from({ length: 16 }, () => macBuf),
  ]);

  await new Promise<void>((resolve, reject) => {
    const sock = createSocket("udp4");
    sock.once("error", reject);
    sock.bind(() => {
      sock.setBroadcast(true);
      // port 9と7の両方に送る
      sock.send(packet, 9, broadcastIp, (err1) => {
        sock.send(packet, 7, broadcastIp, (err2) => {
          sock.close();
          const err = err1 ?? err2;
          err ? reject(err) : resolve();
        });
      });
    });
  });
}

実装上のポイント:

  • **sock.setBroadcast(true)**を忘れるとEACCESエラーが発生します
  • sock.bind()でエフェメラルポートにバインドさせる(明示的なポート指定は不要)
  • createSocket("udp4")でIPv4を明示する(省略すると環境依存になる可能性がある)

sudoなしで実行できるのは、UDPソケット作成に特権が不要だからです。LinuxでもmacOSでも同じコードが動作します。

Magic packet送信後の確認プロセス

サーバーハードウェアが起動するプロセスの様子

Magic packetを送信しただけでは、受信側が実際に起動したかどうかを確認できません。

「3秒待てば起動するだろう」という推測は危険です。実際の復帰時間は大きく異なります。

【復帰所要時間の比較】

復帰元の状態 本環境での実測値 一般的な目安(環境依存のため参考値)
スリープ(S3) 8〜12秒 5〜15秒
休止状態(S4) 18〜25秒 15〜30秒
完全停止(S5) 約45秒 30〜90秒

さらに厄介なのは、WoL信号をNICが拾い損ねたケース(ブロードキャスト未到達、NIC設定漏れなど)では、マシンは永遠に起動しません。

実運用では「対象サービスが応答するまでポーリングし、タイムアウトしたら処理を中断する」というprobe-and-waitロジックが不可欠です。

ComfyUIを例にしたprobe実装

起動対象がComfyUI(RESTful API)なら、/system_statsというヘルスチェック用エンドポイントを叩いて応答を確認します。

async function probeAlive(baseUrl: string, timeoutMs: number): Promise<boolean> {
  const controller = new AbortController();
  const t = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const res = await fetch(`${baseUrl}/system_stats`, {
      signal: controller.signal,
    });
    return res.ok;
  } catch {
    // ネットワーク到達不能またはタイムアウト → 未起動と判定
    return false;
  } finally {
    clearTimeout(t);
  }
}

AbortControllerを使用して2〜3秒でタイムアウトさせることが重要です。fetchのデフォルトタイムアウトは長すぎるため、WoL送信後の待機ループに組み込むと全体の応答時間が悪化してしまいます。

wakeAndWait: 統合的な起動制御

「既に起動していれば送信しない、スリープ中であれば起動させて応答まで待機する」という統合的なロジックを実装します。

async function wakeAndWait(
  baseUrl: string,
  macAddress: string,
  broadcastIp: string,
  options: { probeTimeoutMs?: number; totalTimeoutMs?: number; pollIntervalMs?: number } = {},
): Promise<void> {
  const probeTimeout = options.probeTimeoutMs ?? 2000;
  const totalTimeout = options.totalTimeoutMs ?? 60_000;
  const pollInterval = options.pollIntervalMs ?? 3000;

  // 既に応答があるなら処理をスキップ
  if (await probeAlive(baseUrl, probeTimeout)) return;

  // Magic packetを送信
  await sendMagicPacket(macAddress, broadcastIp);

  // 起動を待機(最大totalTimeoutミリ秒)
  const deadline = Date.now() + totalTimeout;
  while (Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, pollInterval));
    if (await probeAlive(baseUrl, probeTimeout)) return;
  }

  throw new Error(
    `WoL送信後 ${totalTimeout}ms 経過しても ${baseUrl} が応答しませんでした`,
  );
}

設計上の判断ポイント:

  • プローブ先行:既に起動しているマシンへのmagic packet送信は無害ですが、事前プローブで判断できれば送信を省略できます
  • タイムアウト分離:2秒のプローブと3秒のポーリング間隔を分けることで、プローブ自体が長引かないようにします
  • トータル60秒:環境依存のため参考値ですが、スリープからは20秒、シャットダウンからは30〜60秒が一般的な目安です。この時間内に応答しない場合は、呼び出し側で別経路を選択するなどの対応を検討します

content-factoryでの統合

複数の画像を効率的に自動生成するGPUレンダリングシステム

運用上、記事1本あたりアイキャッチ1枚 + セクション4枚 × 候補3枚 = 合計12枚の画像を生成します。この一連の処理において、最初の1枚生成時のみWindows機を起動するコストを支払い、残りは起動済み状態で処理します。

ComfyUIを操作するプロバイダクラスのgetImage()メソッドでwakeAndWaitを呼び出すだけです。

export class ComfyUIProvider {
  constructor(
    private readonly baseUrl: string,
    private readonly logger: Logger,
    private readonly checkpoint: string,
    private readonly wol: WakeOnLanConfig | null,
  ) {}

  async getImage(params: ImageSearchParams): Promise<ImageResult> {
    if (this.wol?.enabled) {
      await this.wakeAndWait();
    }
    // 通常のワークフロー送信・ポーリング...を続行
  }
}

1枚目の生成は起動処理を含むため時間を要しますが、2枚目以降はプローブで即座に起動を確認できるため、追加のオーバーヘッドはほぼありません。結果として12枚中11枚は起動済み前提で処理できる設計になります。

設定のDB管理

WoLの有効/無効、MACアドレス、ブロードキャストIPはデータベース設定として管理し、UIから変更できるようにしました。

image.comfyui_wol_enabled   = true
image.comfyui_wol_mac       = CC:28:AA:XX:XX:XX
image.comfyui_wol_broadcast = 192.168.1.255

環境変数に記述すると変更のたびに再起動が必要ですが、DB設定であればリロード時に即座に反映できるため、運用の柔軟性が向上します。

失敗時のUI通知

wakeAndWaitがタイムアウトした場合、例外をスローします。これを上位のリクエストハンドラでExternalServiceError(502)に変換し、UIに「画像生成に失敗しました」というトースト通知を表示します。

かつて「エラーなしに画像が切り替わらない」というサイレントエラーを経験しました。タイムアウトは必ず明示的なエラーとして伝播させることが重要です。

運用チューニング

実運用で役立つ設定を2点紹介します。

自動スリープの活用

Windowsで「一定時間無操作でスリープ」を設定すれば、使用終了後に自動でスリープ状態へ移行します。

# 電源接続時: 30分でスリープ
powercfg /change standby-timeout-ac 30
# ディスプレイは10分でOFF
powercfg /change monitor-timeout-ac 10

これをBATファイルに記述し、起動時に実行する構成でも十分機能します。

消費電力の削減効果

RTX 3070がアイドル状態でも、ComfyUIがモデルをVRAMにロードしたままなら、GPU消費電力は約30W(本環境での実測値)です。画像生成時は200〜220Wに跳ね上がります。スリープ中は2〜3Wまで低下するため、1日20時間スリープさせた場合、月間の電気代削減額は300〜400円程度(一般的な目安)が期待できます。自宅サーバー運用において、WoLの導入は電気代と騒音の両立削減が可能な実用的な手段です。

総括

ネットワークで接続された複数のサーバーシステム

  • Magic packetは送信だけでは足りません。起動確認のポーリングまで含めた実装が必須です
  • Windows側では3つの要素が必要:電源状態(S0ix / S3 / S5)、NICドライバ設定、高速スタートアップの状態。一つ欠けても動作しません
  • UDPport 9とport 7の両方に送信することで、ネットワーク経由での取りこぼしを防ぎます
  • プローブ先行 → 送信 → ポーリングという流れを採用すれば、既に起動しているマシンへの余計なオーバーヘッドをゼロにできます
  • 失敗時は明示的なエラーとして伝播させることで、サイレントエラーを回避します

WoLは古典的な技術ですが、モダンなアプリケーションへの組み込みにより応用範囲が広がります。NAS、バックアップサーバー、メディアサーバーなど、「普段はスリープ、必要時のみ起動」というパターンは様々な用途に転用可能です。

この実装に興味を持たれた方は、コードを自由にご利用ください。PythonやGoなど、他の言語での実装も同じロジックで構築できるシンプルな仕組みです。


GitHub Repository:WoL Controller実装

関連記事:FLUX.1 schnellをComfyUIで最適化する設定ガイド

この実装が動いた・動かなかった環境や、追加の工夫がある場合は、ぜひコメント欄で教えてください。

コメント