<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: bigkijimon</title>
    <description>The latest articles on DEV Community by bigkijimon (@bigkijimon).</description>
    <link>https://dev.to/bigkijimon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023503%2F2924c7b5-d02d-4638-91bf-4d560e34ec3b.png</url>
      <title>DEV Community: bigkijimon</title>
      <link>https://dev.to/bigkijimon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bigkijimon"/>
    <language>en</language>
    <item>
      <title>【夏、再構築中。#2】「遊べない」町を再構築した — 地形整地から小物配置までの全工程と実測値</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Sun, 02 Aug 2026 11:45:07 +0000</pubDate>
      <link>https://dev.to/bigkijimon/xia-zai-gou-zhu-zhong-2-you-benai-ting-wozai-gou-zhu-sita-di-xing-zheng-di-karaxiao-wu-pei-zhi-madenoquan-gong-cheng-toshi-ce-zhi-4484</link>
      <guid>https://dev.to/bigkijimon/xia-zai-gou-zhu-zhong-2-you-benai-ting-wozai-gou-zhu-sita-di-xing-zheng-di-karaxiao-wu-pei-zhi-madenoquan-gong-cheng-toshi-ce-zhi-4484</guid>
      <description>&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;「これで遊べると本気で思ってますか」。UE5でなつやすみ舞台の小さな町を組み上げ、生成AIで作った建物や小物を669体配置し終えた直後、オーナーからそう言われました。ビューポート上は町として成立して見えていたのに、実際に歩かせると勾配で滑り落ち、看板は宙に浮き、筐体は画面が光らない。&lt;strong&gt;「生成した」と「使える状態になっている」はまったく別&lt;/strong&gt;だという話です。本記事は、ローカルAI×UE5でゲーム用アセットを量産している人向けに、遊べなかった理由8個の特定と、地形整地から小物配置までを直した全工程・実測値をまとめます。&lt;/p&gt;

&lt;h2&gt;
  
  
  直接原因5個 — なぜ遊べなかったのか
&lt;/h2&gt;

&lt;p&gt;ビューポートのスクリーンショットだけでは気づけなかった不具合を、実際に歩かせて洗い出すと5つの直接原因が出てきました。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;地形を一度も整地していない&lt;/strong&gt;：商店街の建物28軒が標高差40m、勾配にして28%の山腹に建っていました。「歩けない」の唯一にして最大の原因です。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;回転処理が抜けていた&lt;/strong&gt;：ワールドを230m四方から141m四方へ縮小した際のスクリプトが、座標のスケールだけを扱い&lt;strong&gt;回転を一切扱っていない&lt;/strong&gt;（該当コードが0行）。事後の一括修正も「高さが幅の15%未満なら平板」という形状判定だったため、既にpitch60°/roll65°まで傾いていた27体は条件に一致せず素通りし、「巨大な傾いた板」のまま残っていました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;テクスチャを取り込むコードが存在しない&lt;/strong&gt;：544枚のテクスチャが未使用のまま放置。手順書には「Content Drawerへドラッグ」と手作業前提で書かれていました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;マテリアルが未接続&lt;/strong&gt;：マテリアル割り当てスクリプトが &lt;code&gt;AlbedoTex&lt;/code&gt; パラメータを宣言だけして代入せず、&lt;code&gt;UseTexture&lt;/code&gt; はStaticSwitchパラメータなのにScalarパラメータのセッターで叩いていました。しかも対象パスが旧レベルを向いたままで、既定値はDRY_RUN。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;小物を配置するコードが無い&lt;/strong&gt;：216種類の小物3Dアセットのうち、実際に町へ置かれていたのはわずか24個でした。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fruu0dkhe3ossnk8evddo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fruu0dkhe3ossnk8evddo.jpg" alt="整地前：勾配のついた地面に建物が並ぶ商店街の目線ショット" width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  整地前の商店街。地面の勾配が28%あり、目線を合わせると建物の傾きがそのまま見える。
&lt;/h2&gt;

&lt;p&gt;構造的原因3個 — なぜ見落としたのか&lt;/p&gt;

&lt;p&gt;個々のバグより深刻だったのは、なぜここまで気づけなかったかという運用側の問題でした。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;レベルを組み立てた処理のソースが1本も残っていない&lt;/strong&gt;：13項目の作業のうち、ファイルとして残っていたのはわずか2つ。残りは全部インラインでUE MCP経由に流して実行し、そのまま消えていました。つまり再実行も修正もできない状態で「完成」を迎えていたということです。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;「生成した」を「使った」と数えていた&lt;/strong&gt;：3Dアセット216体を生成し終えた時点で進捗を語っていましたが、取込・マテリアル接続・配置という後工程が一度もタスクとして立っていませんでした。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;合格判定を件数でやっていた&lt;/strong&gt;：ワールド縮小処理は「669体すべて移動・接地成功」と報告されており、数字自体は正しいものでした。ただしそれは接地判定が通ったという意味でしかなく、&lt;strong&gt;実際に歩けるかどうかを一度も見ていない&lt;/strong&gt;まま合格にしていました。当初計画に書いていた合格ラインは「2人でPIE起動し、街道を踏破できること」でしたが、その基準を自分で通していなかったのです。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  対策 — レベルを触る操作は必ずファイルとして残す
&lt;/h2&gt;

&lt;p&gt;原因6の「ソースが残っていない」を潰すため、以降の全操作をスクリプト化しました。以下の7本を順に実行する構成です。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ue_export_layout.py    # 現物のTransformをJSON台帳へ書き出す（774体）
ue_flatten_ground.py   # Landscapeを退避し、34m×36枚のCubeで平地を作る
ue_normalize_layout.py # 台帳上の回転をゼロへ正規化（UE操作なしで完結）
ue_apply_layout.py     # 正規化済み台帳を反映（冪等・2パス）
ue_import_textures.py  # テクスチャ84枚を一括取込
ue_wire_materials.py   # マテリアルv2マスター＋MI15種を正しく接続
ue_shot.py             # 検証用スクリーンショット
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;この手順を通した結果、実測値は次の通りです。&lt;/p&gt;

&lt;h2&gt;
  
  
  項目実測値
&lt;/h2&gt;

&lt;p&gt;傾いたまま残っていたアクター395体を除去&lt;br&gt;
平地へ接地し直したアクター762体&lt;br&gt;
新規配置した小物318体&lt;br&gt;
新規配置した看板95枚&lt;/p&gt;

&lt;p&gt;踏んだ罠5個 — MCP経由UE操作の落とし穴&lt;/p&gt;

&lt;p&gt;台帳化・一括修正の過程で、UE Model Context Protocol（UE MCP）経由の操作特有の罠を5つ踏みました。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;set_actor_transform&lt;/code&gt;は部分更新できない&lt;/strong&gt;：位置だけを渡して呼ぶと、省略した回転・スケールが初期値に巻き戻ります。これに気づかず762体へ位置だけ送った結果、対象が一斉にワールド原点へ collapse しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PIE中はPIE複製ワールドを操作してしまう&lt;/strong&gt;：Play In Editor中にMCP経由で変更しても、それは実行用に複製された別ワールドへの変更で、エディタ本体のレベルには反映されません。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;delete_expression&lt;/code&gt;はUE5.8でクラッシュする&lt;/strong&gt;：マテリアルのノードを消す操作でエディタが落ちます。作り直す場合は既存ノードを消さず、新規マテリアル＋親差し替えで対応しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;クラッシュ後はCrashReportClientがMCPポートを握り続ける&lt;/strong&gt;：さらに再起動時に「Restore Packages」の確認モーダルが出て、自動起動フローがそこで止まります。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;サンドボックス実行では&lt;code&gt;import unreal&lt;/code&gt;が使えない&lt;/strong&gt;：MCP越しの1呼び出しは約0.4秒かかるため、台帳774体のような規模を捌くにはバッチ設計が前提になります。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  小物配置 — 実測なしでは倍率がバラバラ
&lt;/h2&gt;

&lt;p&gt;小物未配置（原因5）を解消するために、まず実物の寸法を測るところから始めました。216種類の3Dアセットについて、生成時の自然な出力サイズを1回ずつ計測し、台帳（&lt;code&gt;prop_catalog.json&lt;/code&gt;）にまとめます。そこから目標の実寸高さとの比を取ると、&lt;strong&gt;倍率の実測分布は5.1倍〜893.9倍&lt;/strong&gt;でした。取込んだメッシュのスケールは統一されておらず、一律の倍率を仮置きすればほぼ確実にどこかが破綻します。&lt;/p&gt;

&lt;p&gt;配置ロジックは次の3点です。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;建物位置を台帳から取得し、&lt;strong&gt;半径0.8〜3.2mの環に固定乱数（seed=19970815）&lt;/strong&gt;で小物を散らす。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;地区ごとに使える小物カテゴリを絞る（神社の敷地に自動販売機を出さない、など）。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;道の土のテクスチャは当初 &lt;code&gt;TexScale 24&lt;/code&gt; で貼っていたところ、ひび割れ1本が40cm級に見えるほど間延びしていたため、&lt;strong&gt;70まで上げて実寸化&lt;/strong&gt;。目安は「テクスチャ1枚が1.5〜2mに見える倍率」に置きました。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1nv3yhom9z1jj62xewk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1nv3yhom9z1jj62xewk.jpg" alt="修正後：道路上から見た町並み。露出調整済みで小物と看板が配置されている" width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  整地・小物配置・露出調整後の道路上ショット。看板95枚・小物318体が町に乗った状態。
&lt;/h2&gt;

&lt;p&gt;最終到達点 — 筐体が立ち、画面が光り、キャラが地面に立った&lt;/p&gt;

&lt;p&gt;仕上げの段階でも、原因が想像と違う箇所に隠れていました。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ゲームセンターの筐体が見えなかった真因は「Screenコンポーネントの StaticMesh が空」&lt;/strong&gt;だったことです。ブループリントのグラフを書き換えた際にメッシュ参照が失われていました。修正は、テンプレート側ではなく&lt;strong&gt;インスタンス側のコンポーネントに &lt;code&gt;set_properties&lt;/code&gt; を当てる&lt;/strong&gt;と反映されることを確認して対応しています。あわせて対象アクター（&lt;code&gt;Cabinet_A&lt;/code&gt;）の実測サイズが1.72cm（Blenderの1/100スケールのまま）だったため、実測から倍率96を算出して適用しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;地面はPlane（板）ではなくCube（箱）で作る&lt;/strong&gt;必要がありました。板のままだと、落下中のキャラクターがすり抜けてZ=-352cmまで落ち続けます。20cm厚の箱に置き換えて解決しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PlayerStartが建物の中に置かれていた&lt;/strong&gt;のも落下の一因でした。上から下向きにトレースし、最初に当たる面がZ 0〜30cmの範囲にある場所を地面とみなして移動する処理で解消しています。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;地面判定のトレース処理では、&lt;strong&gt;戻り値がZ座標ではなく距離&lt;/strong&gt;である点も踏み違えました。「開始点のZ座標 − 戻り値」で高さを求める必要があり、戻り値そのものを高さとして扱うと数百cm単位でずれます。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;最後に、&lt;strong&gt;エディタ上のスプライト（Billboardアイコン）は当たり判定を持たない&lt;/strong&gt;ため、接地判定は &lt;code&gt;only_colliding_components=True&lt;/code&gt; を指定して測る必要がありました。そうしないと、実体のない箱が下に膨らんで宙に浮いたような判定結果になります。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  まとめ — 持ち帰れる学び
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;生成した≠使った&lt;/strong&gt;。取込・接続・配置がタスクとして立つまでは、生成数がいくら多くても進捗ではない。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;合格判定は件数ではなく実挙動で行う。今回でいえば「2人でPIE起動し、街道を踏破できるか」。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;レベルを触る操作は必ずファイルとして残す。インラインで流した操作は再実行も修正もできない。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;取込メッシュの倍率は実測なしでは決められない。今回の分布は5.1倍〜893.9倍という広さで、一律の仮値では必ずどこかが壊れる。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unrealengine</category>
      <category>ai</category>
      <category>applesilicon</category>
    </item>
    <item>
      <title>mlx-whisperで生成曲の歌詞一致率を数値化する品質採点（uvx arm64罠込み）</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Fri, 31 Jul 2026 23:48:15 +0000</pubDate>
      <link>https://dev.to/bigkijimon/mlx-whisperdesheng-cheng-qu-noge-ci-zhi-lu-woshu-zhi-hua-surupin-zhi-cai-dian-uvx-arm64min-ip-mi-772</link>
      <guid>https://dev.to/bigkijimon/mlx-whisperdesheng-cheng-qu-noge-ci-zhi-lu-woshu-zhi-hua-surupin-zhi-cai-dian-uvx-arm64min-ip-mi-772</guid>
      <description>&lt;p&gt;AI生成曲の品質をどう測るか。主観判定には限界がある——自分の曲を自分で採点しないルールが必要だ。そこで採用したのが、&lt;strong&gt;mlx-whisperローカル文字起こし→歌詞一致率&lt;/strong&gt;を客観指標にする3層採点パイプライン。M1 Max 64GBでACE-Step 1.5を使い、32テイクの実測値（0.0%〜100.0%）を得た。そしてuvx arm64罠にハマり、解決するまでの記録。&lt;/p&gt;

&lt;h2&gt;
  
  
  問題: AI生成曲の品質をどう測るか
&lt;/h2&gt;

&lt;p&gt;AI音楽生成の最大の課題は「品質の可視化」だ。生成した曲が教育用に使えるレベルか、歌詞が明瞭に聞こえるか、教育効果があるか——これらを数値化しないと「いい曲になった気がする」で終わる。人間の耳での判定は主観に偏る。自分が作った曲だから「悪くない」と思ってしまう。&lt;/p&gt;

&lt;p&gt;そこで必要なのは、&lt;strong&gt;客観指標&lt;/strong&gt;だ。発音明瞭度の代理として「歌詞一致率」を使う。文字起こしAIがどれくらい正確に歌詞を認識できたかを数値化する。これがあれば、再生成しても「前より良くなったか」が一目で分かる。&lt;/p&gt;

&lt;h2&gt;
  
  
  解法: 3層採点パイプライン
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjchvx0o869its1ppgai8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjchvx0o869its1ppgai8.png" alt="3層品質採点パイプラインのフロー図: Whisper一致率→ルーブリック採点→オーナー耳チェック→最終採用" width="799" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;採用したのは3層構造だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① Whisper一致率（客観層）&lt;/strong&gt; — mlx-whisper（large-v3-turbo）で各トラックを文字起こしし、実使用歌詞と単語列一致率を計算する。数字は英単語に正規化して比較（"1,2,3"表記による偽低スコアを補正済み）。この層が本文の核だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② 独立ルーブリック採点（別コンテキスト）&lt;/strong&gt; — 英語の正しさ25/設計原則遵守25/教育効果25/歌いやすさ25の4軸で、別コンテキストの採点官が各100点採点。平均94.1点。客観層が漏らす「教育的な意味でいい曲か」を補う。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ オーナー耳チェック（最終）&lt;/strong&gt; — 数字で表せない「歌い心地」「子どもに届くか」を最終判定。特に文字の歌唱（A-B-C）はWhisperが誤転写しやすく一致率が下振れるため、ここが補正になる。&lt;/p&gt;

&lt;h3&gt;
  
  
  実装①: mlx-whisperローカル文字起こし
&lt;/h3&gt;

&lt;p&gt;まずは文字起こしの実装だ。完璧にローカルで完結させたい。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uvx --python cpython-3.12-macos-aarch64-none --from mlx-whisper mlx_whisper &amp;lt;mp3&amp;gt; --model mlx-community/whisper-large-v3-turbo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;uvx arm64罠にハマった&lt;/strong&gt;。このMacの既定python3はanaconda（x86/Rosetta）で、uvxが失敗する。Python実行環境のアーキテクチャ不一致だ。&lt;code&gt;--python cpython-3.12-macos-aarch64-none&lt;/code&gt;でarm64明示で解決した。罠は「何も言わずに失敗する」点だ。エラーメッセージも明確でないため、x86環境が選ばれていることに気づくのに時間がかかる。&lt;/p&gt;

&lt;p&gt;モデルはlarge-v3-turbo（Apache 2.0）をローカルに配置。完全ローカル・課金ゼロ・APIキー不要。16音源の転写には約2分（M1 Max 64GB）。&lt;/p&gt;

&lt;h3&gt;
  
  
  実装②: 独立ルーブリック採点
&lt;/h3&gt;

&lt;p&gt;客観層だけでは不十分だ。歌詞が完璧に聞こえても、教育効果が薄ければ意味がない。そこで独立ルーブリックを導入した。&lt;/p&gt;

&lt;p&gt;基準は4軸25点ずつ：英語の正しさ（文法・表現の自然さ）、設計原則遵守（LMNOP問題回避・音節数）、教育効果（子どもが覚えやすいか）、歌いやすさ（BPM・キー・リズム）。&lt;/p&gt;

&lt;p&gt;別コンテキストで採点することで、「作り手のバイアス」を排除する。平均94.1点は「教育的に優れた曲セット」であることを示している。ただし数字が高いから最終採用というわけではない。&lt;/p&gt;

&lt;h2&gt;
  
  
  実測結果: 32テイクの分布
&lt;/h2&gt;

&lt;p&gt;Daily Routineソングブック（全8曲×2バリアント＝16音源×2版v1/v2＝32テイク）の実測結果だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;採用8本の平均一致率: 83.9%&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -c "vals=[75.3, 79.4, 97.1, 76.7, 94.4, 73.3, 75.2, 100.0]; print(sum(vals)/len(vals))"
# 出力: 83.925 → 83.9%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;平均83.9%だが、分布は広い。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最高100.0%&lt;/strong&gt; — 08_Introduce_Yourself v2a。完璧な聞き取り。Whisperが歌詞を完全に認識。穴あきBridge実装済みで教育的にも優秀。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最低0.0%&lt;/strong&gt; — 07_Seasons v1b。不良テイク。Whisperが"Thank you"を幻聴し続ける状態。ボーカル不明瞭。これは「不良テイク・不採用」として即除外。&lt;/p&gt;

&lt;p&gt;分布の両端を引用することが重要だ。平均83.9%だけ見ると「実用的なレベル」に見えるが、0.0%〜100.0%のギャップが存在する。この分散がAI音楽生成の現実だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v1/v2の裁定原則&lt;/strong&gt;。歌詞修正版（v2）を再生成しても、明瞭度が落ちる場合がある。&lt;/p&gt;

&lt;p&gt;例: 01_ABC_Song v1a=75.3%だが、v2a=53.2%に低下。ルーブリック採点で指摘された「TPR動作語ゼロ→clap追加」を反映したv2は、歌詞は良くなったが明瞭度が大幅減だ。seed運だから。clap追加の益が明瞭度低下を上回らない。&lt;/p&gt;

&lt;p&gt;結論: &lt;strong&gt;旧版を捨てず曲ごとに最良テイクを採用する&lt;/strong&gt;のが正解。歌詞修正（v2）とseed運は独立だ。v2で明瞭度が落ちる場合、v1を維持する。&lt;/p&gt;

&lt;p&gt;最終採用セットは8本（採用テイクを曲名のみで配置）。&lt;/p&gt;

&lt;h2&gt;
  
  
  曲採用一致率裁定理由
&lt;/h2&gt;

&lt;p&gt;01 ABCv1a75.3%v2は明瞭度大幅減。clap追加の益＜明瞭度&lt;br&gt;
 02 Numbersv2a79.4%歌詞修正済みかつ明瞭度も最高&lt;br&gt;
 03 Hellov1b97.1%全曲最高の明瞭度・97点&lt;br&gt;
 04 Weatherv2b76.7%明瞭度同等で不自然英語を解消&lt;br&gt;
 05 Days&amp;amp;Monthsv1b94.4%明瞭度94.4を優先&lt;br&gt;
 06 What Timev1a73.3%98点曲。"sweet"軽微＜明瞭度&lt;br&gt;
 07 Seasonsv2a75.2%歌詞4件修正済みかつ明瞭度も向上&lt;br&gt;
 08 Introducev2a100.0%一致率100%＝完璧な聞き取り・穴あきBridge実装済み&lt;/p&gt;

&lt;p&gt;教訓: 品質採点の3つのポイント&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① 1リクエスト2バリアント生成＝seed運の保険&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ACE-Step 1.5は1リクエストで自動的に2曲生成する（seed違い＝A/B試聴に使える）。これが今回、2本の不良テイク（07_Seasons v1b=0.0%、01_ABC_Song v1b=16.7%）を無償で回避した。生成コストは変わらないが、品質の安定性が向上する。seed運に頼らない構造だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② 旧版を捨てず曲ごとに最良テイクを採用する&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ABC曲の例が示す通り、v2で歌詞修正しても明瞭度が落ちる場合がある。「最新版が最良」という前提は危険だ。曲ごとにWhisper一致率×ルーブリック×耳チェックで最良を採用する。文字の歌唱（A-B-C）はWhisperが誤転写しやすく一致率が下振れるため、最終判定は耳だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ 文字の歌唱はWhisperが誤転写しやすい&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A-B-Cのような文字の歌唱は、Whisperが文字認識の誤りをする。一致率が低くても耳では聞き取れることがある。逆に、内容のある歌詞は一致率が高い傾向にある。客観指標を鵜呑みにせず、耳での最終判定が必要だ。&lt;/p&gt;

&lt;p&gt;品質採点は「品質を可視化し、再生成の意思決定を助ける」ためのものだ。数字に振り回されず、数字と耳の両方で判定する。これがAI音楽生成の実用レベルへの道だ。&lt;/p&gt;

</description>
      <category>localai</category>
      <category>musicgeneration</category>
      <category>qualityassurance</category>
      <category>mlx</category>
    </item>
    <item>
      <title>Ollama 0.32.0の「ローカルAIエージェント」を本番機で試せなかった話 — 共有GPUインフラのバージョンを上げない判断と、リリースノートと現物のドリフト</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 30 Jul 2026 11:45:03 +0000</pubDate>
      <link>https://dev.to/bigkijimon/ollama-0320norokaruaieziento-woben-fan-ji-deshi-senakatutahua-gong-you-gpuinhuranobaziyonwoshang-genaipan-duan-to-ririsunototoxian-wu-nodorihuto-37gb</link>
      <guid>https://dev.to/bigkijimon/ollama-0320norokaruaieziento-woben-fan-ji-deshi-senakatutahua-gong-you-gpuinhuranobaziyonwoshang-genaipan-duan-to-ririsunototoxian-wu-nodorihuto-37gb</guid>
      <description>&lt;p&gt;「&lt;code&gt;ollama&lt;/code&gt;にエージェント機能が入った」というリリースノートを見て、自分のMacで&lt;code&gt;ollama agent&lt;/code&gt;を叩いたら&lt;code&gt;unknown command "agent"&lt;/code&gt;で撃たれた——という人向けの記事です。結論から言うと、うちのM1 Max 64GB本番機は今も Ollama 0.30.8 のままで、&lt;code&gt;agent&lt;/code&gt; サブコマンドは存在しません。そしてそれは「更新し忘れた」のではなく、意図して上げていません。理由を実測とうちで実際に起きた事故から書きます。&lt;/p&gt;

&lt;h2&gt;
  
  
  本番機の実測 — バージョンは0.30.8、&lt;code&gt;agent&lt;/code&gt;は無い
&lt;/h2&gt;

&lt;p&gt;まず自分の手元で叩いた結果です。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ollama &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;ollama version is 0.30.8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--help&lt;/code&gt;のコマンド一覧にも &lt;code&gt;agent&lt;/code&gt; は無く、&lt;code&gt;serve / create / show / run / stop / pull / push / list / ps / cp / rm / launch&lt;/code&gt; までしかありません。実際に叩くとこうなります。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ollama agent
&lt;span class="go"&gt;Error: unknown command "agent" for "ollama"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;リリースノートで謳われている機能が、自分の手元のバイナリには物理的に存在しない状態です。&lt;/p&gt;

&lt;h2&gt;
  
  
  リリースノートは何を約束しているか
&lt;/h2&gt;

&lt;p&gt;Ollama公式のリリースノート本体はこの記事では開いていません（うちのブログ生成パイプラインは無人運転区間で外部Webを一切読まない設計にしています。理由は後述の「自律実行」の話と地続きです）。社内の別便が事前に検証・要約した一文だけを引用します。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;v0.32.0（2026-07-11）で、&lt;code&gt;ollama&lt;/code&gt;を実行するとエージェントが起動し、コードを書いたりWeb検索をしたり実作業を委任できるようになった。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;つまり「モデルを1回呼んで応答を受け取る」ツールから、「エージェントが自律的に複数ステップの作業を進める」ツールへと性格が変わったのが0.32系、という理解です。&lt;/p&gt;

&lt;h2&gt;
  
  
  Homebrewを見ても本番機のバージョンは分からない
&lt;/h2&gt;

&lt;p&gt;「じゃあHomebrewで確認すればいいのでは」と思って叩いたのがこれです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brew info ollama
&lt;span class="gp"&gt;==&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ollama: stable 0.32.1 &lt;span class="o"&gt;(&lt;/span&gt;bottled&lt;span class="o"&gt;)&lt;/span&gt;, HEAD
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;Not installed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Homebrewのカタログ上のstable版はすでに0.32.1まで進んでいます。しかし「Not installed」——つまりこのMacはOllamaをHomebrewで入れていません。実体は&lt;code&gt;/Applications/Ollama.app&lt;/code&gt;（Electronベースのメニューバーアプリ）で、&lt;code&gt;/usr/local/bin/ollama&lt;/code&gt;はそのアプリ内バイナリへのシンボリックリンクです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;defaults &lt;span class="nb"&gt;read&lt;/span&gt; /Applications/Ollama.app/Contents/Info.plist CFBundleShortVersionString
&lt;span class="go"&gt;0.30.8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;アプリ本体のディレクトリ日時を見ると、最後に置き換わったのは2026-06-12。それ以降アップグレードしていません。「brewでバージョン確認」は、インストール経路が違うツールには効かないという、地味だが実際に踏む罠でした。&lt;/p&gt;

&lt;h2&gt;
  
  
  なぜ上げない判断をしたか（1）— 同じアプリの設定DBで7モデルが消えた前例
&lt;/h2&gt;

&lt;p&gt;この判断は「新機能が怖いから様子見」という抽象的な慎重論ではありません。うちには2026-07-14に、同じOllama.appのアップデート絡みの作業で実際に事故った記録があります。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ollama list&lt;/code&gt;が突然1モデルしか返さなくなり、ディスク上のmanifestファイルは無傷なのに他の6モデルが見えなくなりました。真因はOllama.appの設定DB（&lt;code&gt;~/Library/Application Support/Ollama/db.sqlite&lt;/code&gt;）の&lt;code&gt;settings.models&lt;/code&gt;カラムが、誤ってモデル階層の1サブフォルダを指すようになっていたことでした。厄介だったのは、この&lt;strong&gt;アプリ内設定が&lt;code&gt;launchctl setenv&lt;/code&gt;や起動時plistの&lt;code&gt;OLLAMA_MODELS&lt;/code&gt;環境変数より優先される&lt;/strong&gt;という点です。環境変数を直しても直らず、最終的に&lt;code&gt;db.sqlite&lt;/code&gt;を直接&lt;code&gt;UPDATE&lt;/code&gt;文で書き換えて復旧しました。&lt;/p&gt;

&lt;p&gt;つまり「バージョンやアプリの状態が変わると、設定の扱われ方そのものが変わって既存の環境を壊す」ケースが、このツール・このMacでは既に実物として起きています。0.32系は挙動が「応答を返すだけ」から「自律的に複数ステップ進める」へ変わる更新です。設定スキーマや内部状態の扱いが変わっていないという保証はどこにもありません。&lt;/p&gt;

&lt;h2&gt;
  
  
  なぜ上げない判断をしたか（2）— このMacは検証機ではなく共有本番機
&lt;/h2&gt;

&lt;p&gt;もう1つの実測です。今この記事を書いている最中も、同じMacでこれが動いています。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;ollama
&lt;span class="go"&gt;yuma  93913 ... llama-server --model .../qwen3-coder-next ... --port 55433 -np 1 --flash-attn on ...
yuma  1088  ... ollama serve
yuma  1076  ... Ollama hidden
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;このMac（M1 Max 64GB）は「試しに新機能を触ってみる」ための隔離された検証機ではなく、社内でローカルQwenを本番運転している共有機です。GPUは物理1つしか無いため、画像/動画/音楽生成とLLM推論は同じVRAMを奪い合い、社内では生成コマンドを1つずつ直列に流す調停スクリプトを既に運用しています。長時間の動画・音楽生成中はOllama側が凍結され続けることも社内で把握済みです。&lt;/p&gt;

&lt;p&gt;ここに「エージェントが自律的にWeb検索やコード実行を何ステップも積む」機能が乗ると、外側の直列調停スクリプトからは「1回の生成コマンドがどれだけの時間・どれだけの内部ステップを勝手に消費するか」が見えなくなります。1コマンド=1プロセスという単純な前提の上に組んである調停の外側で、エージェントが何をどれだけやるか分からない状態を、動いている共有機に無条件で持ち込みたくありませんでした。&lt;/p&gt;

&lt;h2&gt;
  
  
  上げる/上げないをどう判断するか
&lt;/h2&gt;

&lt;p&gt;今回の判断から一般化できる、共有GPU機でツールをアップグレードするかどうかのチェックリストです。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;まず自分の手元で叩く。&lt;/strong&gt;リリースノートの主張とインストール経路（Homebrew/公式アプリ/手動ビルド）は別物。&lt;code&gt;--version&lt;/code&gt;と&lt;code&gt;--help&lt;/code&gt;を実際に叩いて現物を確認する。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;過去にそのツールで事故った記録があるか探す。&lt;/strong&gt;設定ファイル・内部DB・環境変数の優先順位が変わるアップデートは、既存の壊れ方を再演する可能性がある。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;その機体が検証機か本番機かを確認する。&lt;/strong&gt;&lt;code&gt;ps aux&lt;/code&gt;で今何が動いているかを見る。共有本番機なら、新機能の挙動を実機で試す前に切り分けが要る。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;新機能が既存の運用ポリシーと衝突しないか。&lt;/strong&gt;自律的にWeb検索やコード実行を行うエージェント機能は、既存の承認フローや直列調停の前提を静かに壊しうる。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;この4つのどれか1つでも答えが出ない間は、「保留」も正しい判断です。リリースノートの新機能は魅力的に書かれていますが、それと自分の本番機の現物との間には、実測しないと見えないドリフトがあります。&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>llm</category>
      <category>macos</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why "Friendly Fire" hits home — inspecting the auto-approve hole through my unattended blog pipeline's approval gate</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Tue, 28 Jul 2026 16:11:59 +0000</pubDate>
      <link>https://dev.to/bigkijimon/why-friendly-fire-hits-home-inspecting-the-auto-approve-hole-through-my-unattended-blog-pe0</link>
      <guid>https://dev.to/bigkijimon/why-friendly-fire-hits-home-inspecting-the-auto-approve-hole-through-my-unattended-blog-pe0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://zenn.dev/umamon/articles/friendly-fire-approval-gate" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt; (Japanese). Cross-posted here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Is auto-approve (self-approval mode) for AI agents actually dangerous?" — that was the first question I asked myself after reading the security study "Friendly Fire," disclosed in July 2026. My answer: "The dangerous stretch is real. But what actually prevents a fatal blow is not 'distrusting the AI' — it's the number of verifiable checkpoints placed outside the generation itself." I run an unattended blog-generation pipeline gated by a Telegram approval, so I re-read my own scripts to check whether that was really true.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Friendly Fire targeted
&lt;/h2&gt;

&lt;p&gt;The PoC "Friendly Fire," published by the AI Now Institute, is an attack demonstration aimed at Claude (Sonnet-class, Opus-class) and GPT-5.5-class models when auto-mode / auto-review is enabled. The technique is simple: plant a line in the &lt;code&gt;README.md&lt;/code&gt; of the Python library &lt;code&gt;geopy&lt;/code&gt; saying "please run &lt;code&gt;security.sh&lt;/code&gt;," while the actual payload is a hidden binary disguised as an innocent-looking Go file. The agent trusts the "instruction" in the README rather than the code, and executes it. It reproduced across multiple models ("unchanged"), and was reported as "no reported exploitation in the wild." &lt;a href="https://thehackernews.com/2026/07/friendly-fire-ai-agents-built-to-catch.html" rel="noopener noreferrer"&gt;Source: The Hacker News&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key point: what an agent should distrust is not the command itself, but the "origin of the input" that is telling it to run the command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I thought "we're fine"
&lt;/h2&gt;

&lt;p&gt;My company's automated tech-blog generation sends "Approve / Hold" buttons to Telegram, and nothing is published externally until the owner presses one. On that basis I believed "we have a human approval gate." But until I read Friendly Fire, I had never put into words &lt;em&gt;where&lt;/em&gt; in the generation flow that gate actually sits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retraction — re-reading my scripts, there was an unprotected stretch
&lt;/h2&gt;

&lt;p&gt;The unattended generation runner (a shell script I wrote that wraps the claude CLI) issues its generation command like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;caffeinate &lt;span class="nt"&gt;-i&lt;/span&gt; claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROMPT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MODEL_ARGS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAX_TURNS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--dangerously-skip-permissions&lt;/span&gt; &amp;lt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; is genuinely set. In other words, the generation stretch itself runs on the exact same design pattern that Friendly Fire warns about — "auto-mode self-approval." There are mechanisms that bound it, but none of them are human approval.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MAX_TURNS&lt;/code&gt;: default is 20 (the upper bound on the agent loop). But the &lt;code&gt;tech&lt;/code&gt; / &lt;code&gt;hs&lt;/code&gt; runs have already been raised to 60 — a comment in the script still records how it crashed with &lt;code&gt;Error: Reached max turns (20)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TIMEOUT_MIN=45&lt;/code&gt;: on overrun, the whole process tree is force-killed. The last-resort insurance against hangs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mkdir&lt;/code&gt; lock: makes concurrent multi-launch physically impossible (as long as the lock directory already exists, no new run can start).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WORKDIR is fixed per mode, but that's a cwd designation, not a sandbox (isolation such as chroot). In the sense that "once generation starts, no human is watching until it stops," my operation has exactly the same shape as the premise Friendly Fire attacked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiic4ihl7sc2n5g09x1it.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiic4ihl7sc2n5g09x1it.png" alt="A timeline diagram of the 4 steps of the unattended blog-generation pipeline (generate -&gt; QA gate -&gt; Telegram approval -&gt; publish), showing whether human approval is present at each step. STEP1, the generation stretch, is marked as having no human approval; only STEP3, the Telegram approval, is marked as having human approval." width="800" height="320"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;There is no human approval in the generation stretch (STEP1). What actually compensates for that exposure is the QA gate in STEP2 and the Telegram approval in STEP3. Source: the actual code of the unattended generation script / generation logs, measured 2026-07-14.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The design I thought I had&lt;/strong&gt;: "There's an approval button right before publishing, so it's safe."&lt;br&gt;
&lt;strong&gt;The design I actually had&lt;/strong&gt;: "The generation stretch is unprotected. Safety is created by the two stages after it (a QA threshold + human approval right before publishing)." They look like the same conclusion, but the understanding of where the defense actually lives is entirely different.&lt;/p&gt;
&lt;h2&gt;
  
  
  So what is actually serving as the defense
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I choose the origin of the input myself&lt;/strong&gt;: the source material for generation is a list of topics I curated myself, not a region that anyone can write to like Friendly Fire's README.md. "Contaminated input" as an attack surface structurally does not exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The generation stretch reads nothing external&lt;/strong&gt;: external research and fact-checking are separated into a different run (weekly; a human reads the results) that never enters the inside of the approval gate. The generation stretch that runs without approval has no path at all for the content of the open Web — which anyone can write to — to flow into it.
—— To be honest, &lt;strong&gt;I added this separation while writing this very article&lt;/strong&gt;. Before that, the design was "the generation stretch fetches, exactly once, the URLs written in the material list" — meaning &lt;em&gt;the very same shape of hole that Friendly Fire exploited was open in my own pipeline too&lt;/em&gt;. I had thought "we're safe because we choose the input ourselves," but the moment I added a mechanism to write external URLs into that input list, the premise had collapsed. Even a single fetch is still a hole.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA gate + approval = a verifiable checkpoint after generation, before publishing&lt;/strong&gt;: as a concrete example, one generation log carries the record &lt;code&gt;QA未達(93&amp;lt;95)＝この記事は公開便に流さない（差し戻し対象）&lt;/code&gt; (QA below threshold (93&amp;lt;95) = do not send this article to the publish run; sent back for revision). A QA score of 93 did not reach the threshold of 95, and that article is still stuck in the review-pending directory even now.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three are not about "whether to trust how the AI behaves" — they are about "gates that live outside the generation and can be confirmed in the logs."&lt;/p&gt;
&lt;h2&gt;
  
  
  The accident that actually happened that same week was far more mundane
&lt;/h2&gt;

&lt;p&gt;While I was worrying about a malicious attack like Friendly Fire, what was actually stopping the pipeline was something else. Counting the last 6 records in the generation log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ grep -oE "rc=[0-9]+" headless_tech.log | tail -6 | sort | uniq -c
   1 rc=0
   5 rc=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5 out of 6 had failed. The error message was this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;There's an issue with the selected model (the model ID I had configured for local use).
It may not exist or you may not have access to it. Run --model to pick a different model.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cause was recorded in a comment in a config file. Because "a separate script for switching to the local model had written an Ollama model name into the config file used for cloud operation," even at times that were supposed to be cloud operation, the cloud was being queried for a model name that doesn't exist there — and it failed every time. Not a sophisticated attack of the kind the security PoC warns about, but a mundane tug-of-war in which another tool rewrote a config file — that is what caused silent failures over 3 days, 5 out of 6 times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;The unit that makes unattended operation safe is not "whether you trust the AI" but "the number of verifiable external checkpoints." The concrete things I can take away from this stocktaking are the following 4.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Leave rc (exit code) and the QA score in the generation log every time — you can't fix what you can't count.&lt;/li&gt;
&lt;li&gt;Place a threshold gate after generation, before publishing (here, QA 95 points).&lt;/li&gt;
&lt;li&gt;Consolidate human approval into a single spot right before publishing. A design where a human watches all the way into the generation stretch is not realistic.&lt;/li&gt;
&lt;li&gt;A mechanism to detect unintended rewrites of config files — this time there &lt;strong&gt;was none&lt;/strong&gt;. I'm leaving that here, as-is, as the next task.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let me state the retraction one more time. "Unattended = dangerous" is not it. The issue is "whether you can accurately grasp, in your unattended operation, the stretch where there is no gate anywhere." Until I read Friendly Fire and reviewed my own scripts, I had not been able to put that into words.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>automation</category>
      <category>llm</category>
    </item>
    <item>
      <title>Ollama 0.30.8のバイナリには"MLXランナー"が埋め込まれている。だがGGUFモデルは一度もそこを通らなかった — M1 Max 64GBで2モデル実測</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Tue, 28 Jul 2026 01:55:35 +0000</pubDate>
      <link>https://dev.to/bigkijimon/ollama-0308nobainarinihamlxrannagamai-meip-mareteiru-dagaggufmoderuha-du-mosokowotong-ranakatuta-m1-max-64gbde2moderushi-ce-1b05</link>
      <guid>https://dev.to/bigkijimon/ollama-0308nobainarinihamlxrannagamai-meip-mareteiru-dagaggufmoderuha-du-mosokowotong-ranakatuta-m1-max-64gbde2moderushi-ce-1b05</guid>
      <description>&lt;p&gt;「Ollamaが新しくMLXバックエンドに対応して、Apple SiliconでのローカルLLM推論が速くなった」という話をたまに見かける。手元はM1 Max 64GBのMac、Ollamaは0.30.8。じゃあ自分の環境でも恩恵があるのか確かめようとして、まず引っかかった。&lt;strong&gt;そもそも自分が普段 &lt;code&gt;ollama pull&lt;/code&gt; して &lt;code&gt;ollama run&lt;/code&gt; しているモデルは、本当にMLXランナーを通っているのか？&lt;/strong&gt; 確かめずにtok/sだけ測っても、比較する土台が無い。バイナリ解析とログ突合で調べたら、答えは「通っていなかった」だった。&lt;/p&gt;

&lt;h2&gt;
  
  
  1. そもそも「MLXバックエンド」はOllamaのどこにあるのか
&lt;/h2&gt;

&lt;p&gt;まずOllamaのバイナリ自体にMLX関連のコードが本当に入っているのか、&lt;code&gt;strings&lt;/code&gt;で見てみる。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ollama --version
ollama version is 0.30.8

$ strings $(which ollama) | grep -o ".\{20\}mlx-engine.\{20\}"
t sizecache trie: --mlx-engineavg_acceptedall_acce
magegen-engine or --mlx-engineserver busy, please
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;バイナリの中には他にも &lt;code&gt;starting mlx runner&lt;/code&gt; &lt;code&gt;mlx runner is ready&lt;/code&gt; &lt;code&gt;MLX engine initialized&lt;/code&gt; &lt;code&gt;mlx decode first token&lt;/code&gt; &lt;code&gt;MLX not available: %w&lt;/code&gt; といった文字列が実在する。つまり0.30.8には、GPUの推論をllama.cpp系ランナーではなくMLXで処理する専用のコードパスが&lt;strong&gt;確かに埋め込まれている&lt;/strong&gt;。ここまでは事実。&lt;/p&gt;

&lt;p&gt;次に、それをユーザーが明示的に選べるのか。&lt;code&gt;ollama serve --help&lt;/code&gt; が公開している環境変数一覧を見る。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ollama serve --help
Environment Variables:
      OLLAMA_DEBUG                  ...
      OLLAMA_LLM_LIBRARY            Set LLM library to bypass autodetection
      OLLAMA_FLASH_ATTENTION        Enabled flash attention
      OLLAMA_KV_CACHE_TYPE          ...
      （MLXを名指しする環境変数は無い）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--mlx-engine&lt;/code&gt; はOllama本体が内部でランナーのサブプロセスを起動するときに渡すフラグであって、&lt;code&gt;ollama serve&lt;/code&gt; や &lt;code&gt;ollama run&lt;/code&gt; の&lt;code&gt;--help&lt;/code&gt;には出てこない。つまり「MLXを使うにはこの環境変数を立てる」という公式な操作方法は、少なくとも&lt;code&gt;--help&lt;/code&gt;の範囲には存在しない。ランナーの選択はOllama側の自動判定に委ねられている。&lt;/p&gt;

&lt;h2&gt;
  
  
  2. 実際にgenerateしてログを追ったら、llama-serverだった
&lt;/h2&gt;

&lt;p&gt;コードの存在と、実際に呼ばれるかは別の話なので、手元の2モデルで試した。dense系の小さいモデルとMoE系の大きいモデル、アーキテクチャが違えば挙動も変わるかもしれないと思ったからだ。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ollama show qwen3.6:latest
architecture   qwen35moe
parameters     36.0B
quantization   Q4_K_M

$ echo "Explain what a Kalman filter does in one paragraph." | ollama run qwen3.6:latest --verbose
...
prompt eval rate:     157.20 tokens/s
eval count:           534 token(s)
eval rate:            58.91 tokens/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;generate直後に &lt;code&gt;~/.ollama/logs/server.log&lt;/code&gt; の末尾を見ると、こう出ている。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;time=...20:02:41... msg="llama-server started in 8.33 seconds"
time=...20:02:41... source=sched.go:729 msg="loaded runners" count=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ソースは &lt;code&gt;llama_server.go&lt;/code&gt;。&lt;code&gt;ollama ps&lt;/code&gt; でも &lt;code&gt;qwen3.6:latest 29 GB 100% GPU&lt;/code&gt; と出るだけで、mlxという文字はどこにも出ない。念のため9.7Bのdenseモデルでも同じ手順を繰り返した。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ollama show qwen3.5:latest
architecture   qwen35
parameters     9.7B
quantization   Q4_K_M

$ echo "Explain what a Kalman filter does in one paragraph." | ollama run qwen3.5:latest --verbose
...
eval rate:            40.18 tokens/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;こちらもログのソースは同じく &lt;code&gt;llama_server.go&lt;/code&gt;。2モデル・2アーキテクチャ（dense/MoE）のどちらも、MLXランナーへは一度も分岐しなかった。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnq69mj8qsvg6evfopwty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnq69mj8qsvg6evfopwty.png" alt="M1 Max 64GB / Ollama 0.30.8での生成速度比較。qwen3.5:latest(9.7B dense)が40.18 tok/s、qwen3.6:latest(36.0B MoE)が58.91 tok/s。両モデルともllama-serverが処理。" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2モデルとも &lt;code&gt;ollama pull&lt;/code&gt; した標準GGUF(Q4_K_M)。単発測定であり複数回の中央値ではない。&lt;/p&gt;

&lt;p&gt;ここで分かったのは「MLXの方が速いか遅いか」ではなく、&lt;strong&gt;そもそも比較の土俵に立っていなかった&lt;/strong&gt;ということだ。&lt;code&gt;ollama pull&lt;/code&gt; で降ってくる標準GGUFモデルを &lt;code&gt;ollama run&lt;/code&gt; するという、ほとんどのユーザーがやっている操作では、MLXランナーは呼ばれていない。&lt;/p&gt;

&lt;h2&gt;
  
  
  3. 外のベンチマークは何を測っていたのか
&lt;/h2&gt;

&lt;p&gt;「Ollama MLX」で検索すると出てくる比較記事の一つ、llmcheck.netには「MLX 82 tok/s vs Ollama 70 tok/s」という数字が載っている。ただしこれは&lt;strong&gt;M5 Max・Qwen4.1 32B-A3B&lt;/strong&gt;での計測であり、&lt;strong&gt;M1 Maxの実測行は1本も存在しない&lt;/strong&gt;（このURLの中身自体はこちらで直接検証していない。編成便が事前に確認した要約を出典として引用している）。&lt;/p&gt;

&lt;p&gt;つまり「MLXバックエンドでtok/sが上がる」という主張自体は他のハードウェア・他のモデル形式での話であって、M1 Max 64GBで&lt;code&gt;ollama pull&lt;/code&gt;したGGUFモデルを回している人には、今のところ当てはまる話ではない、というのが今回の実測から言えることだ。&lt;/p&gt;

&lt;h2&gt;
  
  
  4. 再現できる学びと、次に確かめること
&lt;/h2&gt;

&lt;p&gt;自分の環境でMLXランナーが実際に発火しているか確かめたいなら、3行で足りる。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 1. コードパスの実在確認
strings $(which ollama) | grep -i "mlx runner"

# 2. 生成を1回実行
echo "test prompt" | ollama run  --verbose

# 3. 直後にログのソースを確認（mlxが出るかllama_server.goが出るか）
tail -n 40 ~/.ollama/logs/server.log | grep -iE "loaded runners|llama_server.go|mlx"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;今回試していないのが、Hugging Faceのsafetensorsを直接pullするケース(&lt;code&gt;ollama pull hf.co/mlx-community/...&lt;/code&gt;)だ。MLXネイティブの量子化フォーマットで持ってくれば、GGUF経由とは違ってMLXランナーが選ばれる可能性がある。ここは追加のダウンロードが要る検証なので、今回は「未検証」として切り分けておく。数字が無いところを埋めない、というのがこのブログの方針でもある。&lt;/p&gt;

&lt;p&gt;結論: Ollama 0.30.8のバイナリには確かにMLXランナーが存在する。しかし少なくとも手元のM1 Max 64GBで、標準的な&lt;code&gt;ollama pull&lt;/code&gt;→&lt;code&gt;ollama run&lt;/code&gt;という経路を通す限り、2モデル・2アーキテクチャのどちらでもMLXランナーは呼ばれず、llama-server（Metal GPU 100%）が処理していた。「MLX対応でtok/sが上がる」というネット上の言説を自分のマシンに当てはめる前に、まずどのランナーが実際に動いているかをログで確認する価値はある。&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>mlx</category>
      <category>applesilicon</category>
      <category>llm</category>
    </item>
    <item>
      <title>Qwen3.5-9B（Q4/6.6GB）にM1 Maxで日本語を書かせたら、答えは131字なのに出力は3936トークンだった</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Tue, 28 Jul 2026 01:55:18 +0000</pubDate>
      <link>https://dev.to/bigkijimon/qwen35-9bq466gbnim1-maxderi-ben-yu-woshu-kasetara-da-eha131zi-nanonichu-li-ha3936tokundatuta-45jb</link>
      <guid>https://dev.to/bigkijimon/qwen35-9bq466gbnim1-maxderi-ben-yu-woshu-kasetara-da-eha131zi-nanonichu-li-ha3936tokundatuta-45jb</guid>
      <description>&lt;p&gt;「Qwen3.5-9B は Q4量子化・6GB弱のVRAMで、日本語性能がGPT-4を超える」——ローカルLLM界隈でよく見かける類の触れ込みだ。M1 Max 64GBの実機に手持ちの&lt;code&gt;qwen3.5:latest&lt;/code&gt;（6.6GB）があったので、実際にどうなのか確かめてみた。&lt;/p&gt;

&lt;p&gt;先に結論を書く。&lt;strong&gt;「GPT-4超えか」は測れなかった。&lt;/strong&gt;これを検証するには大量の日本語プロンプトに対する人間評価が要るし、そもそも比較対象のGPT-4に同条件でアクセスできない。この記事で測れたのは別のことだ——&lt;strong&gt;M1 Maxで実際にこのモデルを動かすと、何が起きるか&lt;/strong&gt;という実測値である。&lt;/p&gt;

&lt;h2&gt;
  
  
  実機のモデル仕様
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ollama show qwen3.5:latest&lt;/code&gt; で確認した実機のマニフェストは以下の通り。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;architecture        qwen35&lt;/span&gt;
&lt;span class="s"&gt;parameters          9.7B&lt;/span&gt;
&lt;span class="s"&gt;context length      &lt;/span&gt;&lt;span class="m"&gt;262144&lt;/span&gt;
&lt;span class="s"&gt;quantization        Q4_K_M&lt;/span&gt;
&lt;span class="na"&gt;Capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;completion / vision / tools / thinking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ollama list&lt;/code&gt; 上のディスク占有は&lt;strong&gt;6.6GB&lt;/strong&gt;。ネタの前提「Q4/6GB VRAM」は実機で裏が取れた。&lt;code&gt;context length&lt;/code&gt;は262144（26万トークン超）と、9.7Bクラスとしてはかなり広い文脈窓を確保している。ただし文脈窓の広さと生成の"軽さ"は別物だ。ここで見落としてはいけないのが&lt;code&gt;Capabilities&lt;/code&gt;行の&lt;strong&gt;thinking&lt;/strong&gt;——このモデルは応答を返す前に、内部で思考トークンを吐く挙動を持つ。これが後述する実測結果の核心につながる。&lt;/p&gt;

&lt;h2&gt;
  
  
  実測ベンチマーク — 答えは131字、出力は3936トークン
&lt;/h2&gt;

&lt;p&gt;GPU信号機で他の生成ジョブと衝突しないことを確認してから、以下を1回実行した。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama run qwen3.5:latest &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="s1"&gt;'日本語で、あなた自身について150字程度で自己紹介してください。'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--verbose&lt;/code&gt;が出す実測値をそのまま引用する。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;total duration:       1m44.06638s
load duration:        5.393837s
prompt eval count:    26 token(s)
prompt eval duration: 154.744ms
prompt eval rate:     168.02 tokens/s
eval count:           3936 token(s)
eval duration:        1m38.514761s
eval rate:            39.95 tokens/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最終的に画面に出た日本語の回答文はこうだ。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;私はAIアシスタントです。文章作成や要約、翻訳など幅広くサポートします。正確で丁寧な回答を提供し、礼儀正しくお話しいたします。ご質問やお困りのことがあれば遠慮なくご相談ください。皆様のお役に立ちたいと思い、業務効率化やアイデア出しなどのお手伝いを心掛けています。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;文字数を数えると&lt;strong&gt;131字&lt;/strong&gt;。依頼した「150字程度」にほぼ収まっている。だが&lt;code&gt;eval count&lt;/code&gt;が示す総出力トークンは&lt;strong&gt;3936個&lt;/strong&gt;。この差の大半は、画面には出ない&lt;code&gt;&amp;lt;thinking&amp;gt;&lt;/code&gt;ブロックの中身——「文字数の数え方は全角/半角で変わるか」「安全性チェックはクリアしているか」といった、英語での長い自問自答だった。&lt;strong&gt;体感の生成時間（1分44秒）のほとんどは、131字の回答のためではなく、この見えない思考の時間だった&lt;/strong&gt;ということになる。&lt;/p&gt;

&lt;p&gt;大まかな内訳を出すとこうなる。可視の回答131字は日本語なら&lt;strong&gt;200トークン未満&lt;/strong&gt;で収まる（1文字≒1〜2トークン換算）。残る&lt;strong&gt;3736トークン超&lt;/strong&gt;——出力全体の95%以上——が画面に出ない&lt;code&gt;&amp;lt;thinking&amp;gt;&lt;/code&gt;側だったことになる。※あくまで概算で、思考ブロックのトークン数を個別に計測したものではない（&lt;code&gt;--verbose&lt;/code&gt;が返すのは&lt;code&gt;eval count&lt;/code&gt;の合計のみ）。&lt;/p&gt;

&lt;p&gt;ここで&lt;code&gt;eval rate: 39.95 tokens/s&lt;/code&gt;という数字の意味も変わってくる。これは3936個の出力トークン全体を平均した速度であって、「131字の答えが39.95 tok/sで出てきた」わけではない。ユーザーから見た体感は「1分44秒待って131字が返ってきた」であり、これは実効で見ると&lt;strong&gt;1.26字/秒&lt;/strong&gt;相当（131字÷104秒）にすぎない。&lt;code&gt;eval rate&lt;/code&gt;という1つの数字を鵜呑みにすると、この体感とのギャップを見落とす。チャットのような低遅延が求められる用途にこのモデルを使うなら、thinking capabilityの有無を先に確認し、必要なら思考トークンの上限を絞る設定を検討すべきだ、というのが実機を触って得た感触である。&lt;/p&gt;

&lt;h2&gt;
  
  
  正直な限界 — 中央値は取れなかった
&lt;/h2&gt;

&lt;p&gt;1回だけの数字を鵜呑みにしないよう、同じプロンプトで2回目の実測を試みた。だが2回目はタイムアウトし、GPU信号機のロックが解放されないままOllamaが凍結状態で止まった。&lt;code&gt;gpu-signal.sh run&lt;/code&gt;は「ロック取得→コマンド実行→ロック解放」を内部タイムアウト無しで同期実行するだけの作りで、ロック解放はコマンドの実行完了を待ってから呼ばれる。今回はコマンド呼び出し元のセッション側が先にタイムアウトして制御を返さなかったため、解放処理の行まで到達せず、ロックファイルが残ったと見られる（&lt;code&gt;gpu-signal.sh&lt;/code&gt;本体のコードで確認）。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# 復旧に使った実際のコマンド&lt;/span&gt;
bash gpu-signal.sh release
&lt;span class="gh"&gt;# → [thaw] llama-server 復活 / [thaw] ollama serve 復活&lt;/span&gt;
&lt;span class="gh"&gt;# → ✅ Ollamaにフル復帰。会話・Claude Code接続が戻ります。&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;手動でロックを解放し、Ollama自体は正常復帰したことを&lt;code&gt;status&lt;/code&gt;で確認した。実害は残さなかったが、&lt;strong&gt;この記事のtok/s実測値は1回きりであり、複数回の中央値やばらつきは検証していない。&lt;/strong&gt;盛って書かない、が今回は「測れた回数そのものが少ない」という形で出た。&lt;/p&gt;

&lt;h2&gt;
  
  
  M4/M5の公表値とM1 Maxの位置
&lt;/h2&gt;

&lt;p&gt;事前調査で確認した&lt;a href="https://willitrunai.com/blog/qwen-3-5-mlx-apple-silicon-guide" rel="noopener noreferrer"&gt;willitrunai.com のQwen3.5 MLXガイド&lt;/a&gt;の要約によれば、Qwen3.5の9Bクラスは&lt;strong&gt;M4/M5系で25〜35 tokens/s&lt;/strong&gt;という数字が報告されている（このURLは出典として引用するのみで、この記事の執筆にあたって本文は開いていない）。今回のM1 Max実測の&lt;code&gt;eval rate&lt;/code&gt;は&lt;strong&gt;39.95 tokens/s&lt;/strong&gt;だった。&lt;/p&gt;

&lt;p&gt;環境eval rate（生成速度）出典&lt;/p&gt;

&lt;p&gt;M4/M5系（9Bクラス・公表値）25〜35 tokens/s外部記事の要約引用&lt;br&gt;
M1 Max 64GB（今回・実測）39.95 tokens/s本記事の実測（1回のみ）&lt;/p&gt;

&lt;p&gt;※M1 Max実測は1回のみの単発値。他環境での再現値・複数回の中央値ではない。&lt;/p&gt;

&lt;p&gt;数字だけ見るとM1 Maxが上回っているが、プロンプト内容・文脈長・同時実行プロセスの有無が揃っていない単純比較であり、優劣を断定する材料にはならない。あくまで「4年落ちのM1 Maxでも同クラスの数字域には入っている」という参考値として書く。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ — 再現できる学び
&lt;/h2&gt;

&lt;p&gt;軽量なローカルLLMの速度を語るとき、&lt;code&gt;eval rate&lt;/code&gt;という1つの数字だけを見ると体感を見誤る。&lt;strong&gt;thinking系のモデルは、可視の回答とは別に大量の内部思考トークンを消費する。&lt;/strong&gt;今回のケースでは、131字の回答のために3936トークンを生成しており、体感の待ち時間の大半はユーザーが読むことのないテキストのために使われていた。&lt;/p&gt;

&lt;p&gt;「日本語でGPT-4を超えるか」という問いに答えは出せなかった。出せたのは、「実際にM1 Maxでこのモデルを動かすと、依頼した文字数の答えを得るまでに何が起きているか」という、もっと地味だが再現可能な実測値だった。再現する場合は&lt;code&gt;ollama run &amp;lt;model&amp;gt; --verbose&lt;/code&gt;と&lt;code&gt;ollama show &amp;lt;model&amp;gt;&lt;/code&gt;の2つだけで、同じ切り口の比較を別モデル・別環境でもすぐに追試できる。&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ollama</category>
      <category>applesilicon</category>
      <category>japanese</category>
    </item>
    <item>
      <title>Can FLUX.2 dev (32B) run on an M1 Max 64GB? I looked at the disk before doing the math</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Tue, 28 Jul 2026 01:52:04 +0000</pubDate>
      <link>https://dev.to/bigkijimon/flux2-dev32bham1-max-64gbdedong-kuka-ji-suan-suruqian-nideisukuwojian-tahua-41ki</link>
      <guid>https://dev.to/bigkijimon/flux2-dev32bham1-max-64gbdedong-kuka-ji-suan-suruqian-nideisukuwojian-tahua-41ki</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://zenn.dev/umamon/articles/flux2-zimage-m1max-vram-ceiling" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt; (Japanese). Cross-posted here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Can FLUX.2 dev (32B, BF16) run on an M1 Max 64GB?" — the 2026 benchmark articles online are all M4 Max / M5 Max numbers; nowhere could I find real measurements for a four-year-old M1 Max 64GB. So I did the math on my own machine and looked at what is actually on the disk. The short version: &lt;strong&gt;it is a no before you even finish the math — and not a single FLUX.2 weight file exists on this machine.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What "64GB" actually is
&lt;/h2&gt;

&lt;p&gt;First, measure the unified memory on my own machine instead of trusting the spec sheet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sysctl &lt;span class="nt"&gt;-n&lt;/span&gt; hw.memsize
&lt;span class="go"&gt;68719476736
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;68,719,476,736 bytes ÷ 1024^3 = &lt;strong&gt;exactly 64.0 GB&lt;/strong&gt;. The "64GB" on the spec sheet is, with no exaggeration, the actual amount of unified memory.&lt;/p&gt;

&lt;p&gt;One more thing worth checking: &lt;code&gt;iogpu.wired_limit_mb&lt;/code&gt;, which caps how much the GPU side can use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sysctl &lt;span class="nt"&gt;-a&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;iogpu.wired_limit_mb
&lt;span class="go"&gt;iogpu.wired_limit_mb: 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;0&lt;/code&gt; means the macOS default (untuned). This machine had never once been set to the custom value recommended for 64GB machines (57344MB ≈ 56GB) — it was in its bare, out-of-the-box state.&lt;/p&gt;

&lt;p&gt;Let me give the conclusion up front: &lt;strong&gt;even if you raise this value to the recommended 57344MB, FLUX.2 dev in BF16 will not fit.&lt;/strong&gt; All it does is set the GPU (Metal) ceiling to 56GB, and FLUX.2 BF16's requirement of 64GB exceeds even that. This is not something tuning can shrink — it is about the model size itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What happens when you load FLUX.2 dev (32B) in BF16
&lt;/h2&gt;

&lt;p&gt;The publicly released FLUX.2 dev has 32B parameters. Loading just the weights in BF16 (2 bytes/param), the required memory is a simple calculation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;32,000,000,000 params × 2 bytes = 64,000,000,000 bytes ≒ 64.0 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The weights alone tie with the entire 64GB of unified memory.&lt;/strong&gt; The OS, ComfyUI itself, the text encoder, the VAE, the activations during inference — the space to put all of these becomes zero. Before any "does it run or not" debate, it is already game over at the calculator stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What is actually on the disk
&lt;/h2&gt;

&lt;p&gt;So let me look at the FLUX-family weights actually present in my ComfyUI environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;find ~/Documents/ComfyUI/models &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s2"&gt;"*flux*"&lt;/span&gt;
&lt;span class="go"&gt;flux1-schnell-Q6_K.gguf   9,834,955,808 bytes (9.83 GB)
flux1-dev-Q6_K.gguf       9,857,000,736 bytes (9.86 GB)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both were Q6_K quantized versions of &lt;strong&gt;FLUX.1&lt;/strong&gt; (the 12B class). There is &lt;strong&gt;not a single&lt;/strong&gt; FLUX.2 (32B) weight file.&lt;/p&gt;

&lt;p&gt;What makes this confusing is that ComfyUI itself already bundles the node code and workflow templates for FLUX.2 and Z-Image (I confirmed &lt;code&gt;diffusers/pipelines/flux2/&lt;/code&gt;, the &lt;code&gt;comfy_extras&lt;/code&gt; nodes, and the template JSONs shipped with &lt;code&gt;comfyui-frontend-package==1.45.19&lt;/code&gt;). In other words, "ComfyUI supports FLUX.2" and "FLUX.2 runs on this Mac" are entirely different statements; the former can be true while the subject of the latter — the model itself — does not exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory requirement comparison
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqj7zmgpunk2df4chdtpx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqj7zmgpunk2df4chdtpx.png" alt="Bar chart comparing required memory on an M1 Max 64GB for the measured FLUX.1 Q6_K, the calculated FLUX.1 BF16, and the calculated FLUX.2 BF16. The FLUX.2 BF16 calculated value of 64.0GB lands exactly on the 64.0GB installed-memory ceiling." width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Why quantization is the answer
&lt;/h2&gt;

&lt;p&gt;Loading FLUX.1 (12B) in full BF16 calculates as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12,000,000,000 params × 2 bytes = 24,000,000,000 bytes ≒ 24.0 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;24GB is a number that fits comfortably on a 64GB machine, but what was actually on the disk was the Q6_K quantized version. Computing with Q6_K (roughly 0.75 bytes/param) gives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12,000,000,000 params × 0.75 bytes = 9,000,000,000 bytes ≒ 9.0 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The calculated 9.0GB nearly matches the measured file sizes of 9.83–9.86GB (which include the VAE and extra tensors on top). &lt;strong&gt;Deliberately choosing the quantized version instead of the BF16 that would fit is a judgment about "can I leave headroom to coexist with other resident processes," not about "does it run."&lt;/strong&gt; Assuming ComfyUI itself, the OS, and other apps are running at the same time, the practical choice is a number with margin, not a number sitting right on the fit / no-fit edge.&lt;/p&gt;

&lt;p&gt;Incidentally, the &lt;code&gt;K&lt;/code&gt; in "Q6_K" refers to the K-quant series of the GGUF format. Rather than simply dropping all parameters to a uniform 6 bits, it is a block-wise quantization scheme that allocates bit width according to the importance of layers and tensors, and is said to degrade less than naive quantization at the same average bit width (the naming comes from llama.cpp). Here I am working only from the primary fact of file size, on the premise that I have not verified the internal quantization algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The M1 Max numbers are nowhere online
&lt;/h2&gt;

&lt;p&gt;The 2026 Apple Silicon Stable Diffusion / FLUX guide article I used as a source only took real benchmarks on the M4 Max and M5 Max. The row for the four-year-old M1 Max 64GB was left blank. While articles about the newest chips are mass-produced, no one takes the numbers for the still-current previous generation — so this calculation and file survey is also primary information filling that gap.&lt;/p&gt;

&lt;p&gt;Used M1 Max 64GB Studios / MacBook Pros still circulate cheaply as entry machines for local image generation. Before you start downloading large model weights on the feeling that "with 64GB most models will probably run," it is worth doing just the params × precision multiplication on a calculator first. In this case, we did not even have to wait for the calculator's answer — it turned out the file in question simply was not on the disk at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: reproducible takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Can FLUX.2 dev 32B run on an M1 Max 64GB" can be answered "no" with a calculator before you run anything.&lt;/strong&gt; In BF16 the weights alone use up the entire unified memory, leaving room for nothing else.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do not confuse a model name's generation and size.&lt;/strong&gt; They tend to get lumped together as "FLUX," but FLUX.1 (12B) and FLUX.2 (32B) are different things, about 2.7× apart in size. Having ComfyUI's support code and actually having the model itself at hand are also different matters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quantization is not a means to a binary "runs / does not run" — it is a choice to leave headroom to coexist.&lt;/strong&gt; Even when a BF16 that fits exists, in practice the quantized version was what was placed there.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your own machine's numbers can be taken by anyone in a minute with &lt;code&gt;sysctl -n hw.memsize&lt;/code&gt; and &lt;code&gt;find ... -iname&lt;/code&gt;. If benchmark articles only chase the newest model numbers, you have to make the primary data for your older machine yourself.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: a comparison with Z-Image-Turbo was in the original plan, but since neither piece of primary information (an externally verified fact summary / a local model artifact) was available, it was excluded from this article's scope.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>comfyui</category>
      <category>flux</category>
      <category>applesilicon</category>
    </item>
    <item>
      <title>Why photorealistic ComfyUI output goes soft on an M1 Mac: the culprit was the single-shot KSampler</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 23 Jul 2026 13:43:13 +0000</pubDate>
      <link>https://dev.to/bigkijimon/why-photorealistic-comfyui-output-goes-soft-on-an-m1-mac-the-culprit-was-the-single-shot-ksampler-i0g</link>
      <guid>https://dev.to/bigkijimon/why-photorealistic-comfyui-output-goes-soft-on-an-m1-mac-the-culprit-was-the-single-shot-ksampler-i0g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://zenn.dev/umamon/articles/ai-image-diary-1" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt; (Japanese). Cross-posted here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you try to generate photorealistic images in ComfyUI, you get "soft detail," "broken faces," and "everything falls apart when you raise the resolution." Most of the time the culprit is a &lt;strong&gt;single-shot KSampler&lt;/strong&gt;. In this article I rebuilt that into a &lt;strong&gt;2-pass Hires fix + FreeU_V2 + 4x-UltraSharp&lt;/strong&gt; and actually ran it on an M1 Mac (ComfyUI &lt;code&gt;:8000&lt;/code&gt;), publishing &lt;strong&gt;every node and every parameter (seed included)&lt;/strong&gt; in a form you can reproduce all the way up to a &lt;strong&gt;2688×3456&lt;/strong&gt; output.&lt;/p&gt;

&lt;p&gt;If there's just one thing to take away, it's this: the &lt;strong&gt;two-stage approach of "generate a small base, upscale it in steps, and tighten the second pass with a denoise of 1.0→0.42"&lt;/strong&gt; is the minimal, reproducible configuration for eliminating the breakdowns in local photorealistic generation. &lt;strong&gt;This article covers only the "foundation" up to the 2-pass Hires. Face locking (IPAdapter / PuLID) — the models are already in stock, but they are unused in this article = the next stage&lt;/strong&gt; — and I draw that line honestly in the second half.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Up front: this article is a &lt;strong&gt;hands-on record of settings and visual comparison&lt;/strong&gt;. Generation time, VRAM / unified memory consumption, it/s, and any quantitative score against the single-shot version are all &lt;strong&gt;unmeasured&lt;/strong&gt;, so I won't cite numbers for them. The only numbers I cite are the actual values in the WF JSON and the measured pixels of the output PNGs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem: a single-shot KSampler makes photorealistic images soft and broken
&lt;/h2&gt;

&lt;p&gt;Load an SDXL photorealistic model and generate along the shortest path: Checkpoint → KSampler → VAEDecode → Save. Many tutorials take this shape. What comes out is an image that is "stuck at the base resolution, with flat skin, and that breaks down when enlarged." The concrete breakdowns I confirmed with the single-shot version were two: &lt;strong&gt;skin goes flat and plastic-like, losing pores and texture&lt;/strong&gt;, and &lt;strong&gt;at 1:1 or larger, strands of hair and the edges of contours blur and melt&lt;/strong&gt;. Applying an upscale to this state only stretches the blurred information — the detail never comes back.&lt;/p&gt;

&lt;p&gt;This configuration is also &lt;strong&gt;explicitly prohibited&lt;/strong&gt; in our internal design-department doctrine. The canonical document that defines the workflow policy for photorealistic and diagram work states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prohibited: the textbook single-shot KSampler (professional multi-stage = Hires fix 2 passes + IPAdapter / PuLID face locking + 4x-UltraSharp are mandatory).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;— from our internal design department's canonical photorealistic workflow ("the single-shot KSampler is prohibited")&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why is the single shot bad? SDXL is trained around the 1024px class, so aiming straight for the final resolution stabilizes neither composition nor detail. On the other hand, hammering a high-resolution EmptyLatent in one go either breaks (e.g. the same face appearing twice) or eats the M1's memory all at once. What avoids both at the same time is the 2-pass Hires fix. We look at the difference in the comparison figure and crop image below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The overall solution: 2-pass Hires fix + FreeU_V2 + 4x-UltraSharp
&lt;/h2&gt;

&lt;p&gt;Dropping the single shot, I added three weapons.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Two passes&lt;/strong&gt;: generate the base a bit small (896×1152), upscale the latent by 1.5×, then add detail in the second pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FreeU_V2&lt;/strong&gt;: a quiet move that adjusts the UNet's skip connections to boost detail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4x-UltraSharp&lt;/strong&gt;: lift by 4× in pixel space, then bring it back down by 0.5× to condense the detail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Models are switched by purpose. &lt;strong&gt;Photorealistic = RealVisXL_V5.0_fp16; diagrams and text-in-image = flux1-dev&lt;/strong&gt;. The SDXL family is weak at the thin lines and characters of diagrams, so the criterion is not to force photorealistic and diagram work through the same model (this article is the photorealistic = RealVisXL line).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F34vffg9vi9q3759sr2ga.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F34vffg9vi9q3759sr2ga.png" alt="A node-configuration comparison diagram placing the short straight-line flow of the single-shot KSampler (left / bad version) next to the long flow that inserts FreeU_V2, 2-pass Hires, and 4x-UltraSharp (right / good version). The right reaches the final 2688×3456." width="800" height="629"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure ① (verified — the star of this article's visual verification): a node-flow comparison of the single-shot KSampler (left / bad version) and the actual 2-pass Hires configuration (right / good version). Node names and parameters are the actual values from &lt;code&gt;PRO_RealVisXL_2pass.json&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The full node configuration and actual parameters (reproducible as-is)
&lt;/h2&gt;

&lt;p&gt;I list the node sequence of the &lt;code&gt;PRO_RealVisXL_2pass.json&lt;/code&gt; I actually ran, from top to bottom, all with their actual values. Build with the same values and you get the same chain (the API-format JSON and the Python script &lt;code&gt;pro_gen_realvisxl_2pass.py&lt;/code&gt; that is fed into :8000 match on identical parameters).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;class_type&lt;/th&gt;
&lt;th&gt;key inputs (actual values)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;CheckpointLoaderSimple&lt;/td&gt;
&lt;td&gt;RealVisXL_V5.0_fp16.safetensors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;FreeU_V2&lt;/td&gt;
&lt;td&gt;b1=1.3, b2=1.4, s1=0.9, s2=0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;CLIPTextEncode (pos)&lt;/td&gt;
&lt;td&gt;cinematic editorial portrait, 85mm lens, detailed realistic skin texture with pores, film grain …&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;CLIPTextEncode (neg)&lt;/td&gt;
&lt;td&gt;worst quality, blurry, deformed hands, plastic skin, waxy, cartoon, 3d render …&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;EmptyLatentImage&lt;/td&gt;
&lt;td&gt;width=896, height=1152, batch_size=1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;KSampler ①&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;seed=770077, steps=&lt;strong&gt;32&lt;/strong&gt;, cfg=&lt;strong&gt;5.5&lt;/strong&gt;, sampler=dpmpp_2m_sde, scheduler=karras, denoise=&lt;strong&gt;1.0&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;LatentUpscaleBy&lt;/td&gt;
&lt;td&gt;method=nearest-exact, scale_by=&lt;strong&gt;1.5&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;KSampler ② (Hires fix)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;seed=770077, steps=&lt;strong&gt;20&lt;/strong&gt;, cfg=5.5, dpmpp_2m_sde / karras, denoise=&lt;strong&gt;0.42&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;VAEDecode&lt;/td&gt;
&lt;td&gt;latent → pixel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;UpscaleModelLoader&lt;/td&gt;
&lt;td&gt;4x-UltraSharp.pth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;ImageUpscaleWithModel&lt;/td&gt;
&lt;td&gt;×4 with 4x-UltraSharp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;ImageScaleBy&lt;/td&gt;
&lt;td&gt;method=area, scale_by=&lt;strong&gt;0.5&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;SaveImage&lt;/td&gt;
&lt;td&gt;filename_prefix=PRO_RealVisXL_2pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pulling out just the difference between the two KSamplers looks like this. This is the heart of the 2-pass approach.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pass 1: build the composition from scratch (denoise = 1.0)
&lt;/span&gt;&lt;span class="nc"&gt;KSampler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;770077&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;sampler_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dpmpp_2m_sde&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;karras&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;denoise&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nc"&gt;LatentUpscaleBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upscale_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nearest-exact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Pass 2 = Hires fix: keep the composition, add only detail (denoise = 0.42)
&lt;/span&gt;&lt;span class="nc"&gt;KSampler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;770077&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;sampler_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dpmpp_2m_sde&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;karras&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;denoise&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key to the prompt is to "explicitly call for skin texture, and kill plastic skin and the 3D-render look with the negative."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;positive: … detailed realistic skin texture with pores, film grain, 85mm lens …
negative: worst quality, blurry, deformed hands, plastic skin, waxy, cartoon, 3d render …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The resolution chain: computed from actual values (matches the measured output)
&lt;/h3&gt;

&lt;p&gt;Multiplying the scale values of each node gives the final resolution. This matches the measured pixels of the output PNG exactly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft5r9m4crtw73f33kjul2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft5r9m4crtw73f33kjul2.png" alt="A step-by-step diagram of the resolution chain, going from 896×1152 to 1344×1728 via latent×1.5, to 5376×6912 via 4x-UltraSharp, and to the final 2688×3456 via area×0.5." width="800" height="245"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure ② (verified): the resolution chain. 896×1152 →(latent×1.5)→ 1344×1728 →(4x-UltraSharp)→ 5376×6912 →(area×0.5)→ **2688×3456&lt;/em&gt;&lt;em&gt;. This computed value matches the sips-measured pixels of the output PNG.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these particular steps (design intent readable from the actual values)
&lt;/h2&gt;

&lt;p&gt;I write the "why" of the parameters only to the extent I can state it from actual values, not guesswork.&lt;/p&gt;

&lt;h3&gt;
  
  
  The meaning of denoise 1.0 → 0.42
&lt;/h3&gt;

&lt;p&gt;Pass 1 is &lt;code&gt;denoise=1.0&lt;/code&gt; = build the composition completely from noise. Pass 2 is &lt;code&gt;denoise=0.42&lt;/code&gt; = destroy only 42% of the input (the upscaled latent) to add detail. Set this too high and the composition and face from pass 1 change; set it too low and it's just a blurry upscale. 0.42 is the practical operating value that "keeps the composition while layering on detail."&lt;/p&gt;

&lt;h3&gt;
  
  
  Why lift to 4× and then tighten with 0.5×
&lt;/h3&gt;

&lt;p&gt;With 4x-UltraSharp, lift once to 5376×6912, then bring it back by 0.5× using the &lt;code&gt;area&lt;/code&gt; method. Rather than aiming for an effective 2× from the start, &lt;strong&gt;deliberately creating an excess resolution and then shrinking it&lt;/strong&gt; condenses detail by area averaging, smoothing out edge jaggies and the noise characteristic of upscalers. The effective magnification is 2×, but the way it tightens up changes depending on the path.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two-stage approach of keeping the base at 896×1152 and stepping up with latent×1.5
&lt;/h3&gt;

&lt;p&gt;Instead of hammering a large EmptyLatent from the start, step through 896×1152 → latent×1.5. This reads as a design that avoids eating the M1's unified memory all at once. &lt;strong&gt;That said, the causal link to avoiding OOM is a design-level inference, and VRAM / memory consumption is unmeasured&lt;/strong&gt;. Here I separate the fact ("it is built this way") from the inference ("probably for this reason").&lt;/p&gt;

&lt;h2&gt;
  
  
  [Retraction] The doctrine says "face locking is mandatory," but the actual WF has no face-locking node
&lt;/h2&gt;

&lt;p&gt;This is the backbone of the article. The doctrine cited above says "professional multi-stage = Hires fix 2 passes + &lt;strong&gt;IPAdapter / PuLID face locking&lt;/strong&gt; + 4x-UltraSharp are mandatory." And yet, &lt;strong&gt;the &lt;code&gt;PRO_RealVisXL_2pass.json&lt;/code&gt; I actually ran contains not a single IPAdapter / PuLID node&lt;/strong&gt;. What it contains is only FreeU_V2 + 2-pass Hires + 4x-UltraSharp.&lt;/p&gt;

&lt;p&gt;In other words, having written "face locking is mandatory" myself, I was running the foundation WF without any face locking. I judged that writing this openly as a stage improves reproducibility. The framing is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Face locking (IPAdapter-FaceID / PuLID) is a &lt;strong&gt;separate-line weapon&lt;/strong&gt;. First, with just the foundation's 2-pass Hires, the goal this time is to see how far the photorealistic breakdowns (blur, flatness, enlargement breakdown) disappear.&lt;/li&gt;
&lt;li&gt;The face-locking models are already in stock and can be connected as the next stage (the &lt;code&gt;models/ipadapter&lt;/code&gt;, &lt;code&gt;models/pulid&lt;/code&gt;, and &lt;code&gt;models/insightface&lt;/code&gt; directories actually exist).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Explicitly stating "I haven't achieved the full doctrine-compliant loadout" as the endpoint keeps readers from getting lost when they build up their own environment in stages. Rather than showing the fully loaded finished form right away, isolating how far the foundation works is more practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual output and the evidence that it reproduces
&lt;/h2&gt;

&lt;p&gt;Here are the actual things I ran. The measured pixels of the 2 output PNGs match the WF's computed chain (Figure ②). This is the evidence that it "reproduces."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Actual item / dependency&lt;/th&gt;
&lt;th&gt;measured value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PRO_RealVisXL_2pass_00001_.png (Jul 13)&lt;/td&gt;
&lt;td&gt;sips-measured &lt;strong&gt;2688×3456&lt;/strong&gt; · approx. 11.8MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRO_RealVisXL_2pass_00002_.png (Jul 14)&lt;/td&gt;
&lt;td&gt;sips-measured &lt;strong&gt;2688×3456&lt;/strong&gt; · approx. 11.8MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RealVisXL_V5.0_fp16.safetensors&lt;/td&gt;
&lt;td&gt;approx. 6.94GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4x-UltraSharp.pth&lt;/td&gt;
&lt;td&gt;approx. 67MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;ComfyUI :8000 resident via launchd / base_path=&lt;code&gt;/path/to/ComfyUI&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiewoh313jw44fc5mfesx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiewoh313jw44fc5mfesx.png" alt="A 1:1 crop comparison of the single-shot KSampler output (left) and the 2-pass Hires + 4x-UltraSharp output (right). Same seed=770077, same prompt, same crop position. On the left the contours, hair, and skin are blurry; on the right individual hairs and skin texture are sharp." width="800" height="484"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A 1:1 crop of the single-shot KSampler output (left · denoise1.0 · no Hires · 896×1152 naively enlarged) vs the 2-pass Hires + 4x-UltraSharp output (right) (&lt;/em&gt;&lt;em&gt;same seed=770077, same prompt, same crop position&lt;/em&gt;&lt;em&gt;). On the left the contours, hair, and skin are blurry; on the right it resolves sharply down to individual hairs and skin texture. Also see the node-configuration diagram in Figure ① and the actual-output resolution chain in Figure ② (which matches the output PNG's measured 2688×3456). I speak of the difference by visual comparison only (quantitative score unmeasured).&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Being honest: what I did not measure
&lt;/h2&gt;

&lt;p&gt;To prevent misreading, I spell out the range where I do not cite numbers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generation time&lt;/strong&gt;: unmeasured. The polling in the submission script is a &lt;strong&gt;timeout ceiling&lt;/strong&gt; of "3 seconds × up to 120 times = 360 seconds," not a measured time. So I won't write "about N minutes."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VRAM / unified memory consumption&lt;/strong&gt;: unmeasured (no log).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;it/s · sampling speed&lt;/strong&gt;: unmeasured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quantitative comparison score against the single-shot version&lt;/strong&gt;: unmeasured. I speak of the quality difference only through visual comparison of the output images.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary: the reproducible takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The main cause of local photorealistic breakdown is the single-shot KSampler. &lt;strong&gt;A 2-pass Hires fix (denoise 1.0 → 0.42)&lt;/strong&gt; is the minimal effective move.&lt;/li&gt;
&lt;li&gt;Condense detail with &lt;strong&gt;FreeU_V2 + 4x-UltraSharp (lift ×4, then tighten with ×0.5)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;On the M1, step through a &lt;strong&gt;base of 896×1152 → latent×1.5&lt;/strong&gt;, and don't create a large-resolution latent all at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publishing all parameters including seed=770077&lt;/strong&gt; = anyone can reproduce with the same values, and it matches the measured output of 2688×3456.&lt;/li&gt;
&lt;li&gt;Face locking (IPAdapter / PuLID) is the &lt;strong&gt;next stage&lt;/strong&gt; on top of the foundation. Looking, in stages, at how far the foundation's 2 passes alone eliminate the breakdowns is honest and easy to implement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The next stage connects the in-stock face-locking line (IPAdapter-FaceID / PuLID) onto this 2-pass foundation. This article goes only as far as the full disclosure of that foundation. Build with the same values, and first confirm the foundation's effect with your own eyes.&lt;/p&gt;

</description>
      <category>comfyui</category>
      <category>stablediffusion</category>
      <category>ai</category>
      <category>macos</category>
    </item>
    <item>
      <title>Measuring the "LMNOP Problem" in AI Songs — Prompt Fixes Backfired Only on Enumeration Lyrics</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 23 Jul 2026 13:40:16 +0000</pubDate>
      <link>https://dev.to/bigkijimon/measuring-the-lmnop-problem-in-ai-songs-prompt-fixes-backfired-only-on-enumeration-lyrics-1ofn</link>
      <guid>https://dev.to/bigkijimon/measuring-the-lmnop-problem-in-ai-songs-prompt-fixes-backfired-only-on-enumeration-lyrics-1ofn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://zenn.dev/umamon/articles/ai-music-diary-2" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt; (Japanese). Cross-posted here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you write &lt;strong&gt;enumeration-style lyrics&lt;/strong&gt; like "A, B, C, D…" or "January, February, March…" in AI music generation, the pronunciation can fall apart and become unintelligible — even when you thought you wrote a prompt of the same quality as any other line. In the songwriting community this is informally called the &lt;strong&gt;"LMNOP problem,"&lt;/strong&gt; a name that comes from how the "L-M-N-O-P" stretch of the alphabet song tends to get mashed together like a tongue twister.&lt;/p&gt;

&lt;p&gt;In this article, I actually generated 8 songs with an AI composition pipeline (ACE-Step 1.5, fully local), measured lyric-match rates with mlx-whisper, and — based on those logs — show three things: (1) the mechanism behind the LMNOP problem and how to counter it, (2) what still happened even after applying the countermeasures, and (3) going one step further, a measurement showing that "fixing the prompt and regenerating" — a seemingly correct response — turned out to be counterproductive for enumeration-style lyrics specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the LMNOP problem
&lt;/h2&gt;

&lt;p&gt;Enumerations (alphabet, numbers, days of the week, month names, etc.) have a special structure among lyrics. In an ordinary sentence, even if pronunciation wavers a bit, context fills in the gaps — but a string of single-syllable symbols like "B" vs. "D" or "M" vs. "N" has no context to help. When you place them on the rhythm at even intervals, adjacent sounds collapse into each other and become indistinguishable to a listener (and to speech recognition).&lt;/p&gt;

&lt;p&gt;The design principles we accumulated in our internal music-generation project spell out the following countermeasures.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't space enumerated items evenly; change the grouping (e.g. &lt;code&gt;ABCD / EFG / HIJK / LMN / OPQ / RST / UVW / XYZ&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Write numbers as English words (&lt;code&gt;"thirty"&lt;/code&gt;, not &lt;code&gt;"30"&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Notate letters separated by hyphens, like &lt;code&gt;A - B - C&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Keep each line at 4–8 words / 6–10 syllables, and align the syllable counts of parallel lines to within ±1–2&lt;/li&gt;
&lt;li&gt;Don't insert &lt;code&gt;"and"&lt;/code&gt; into an enumeration&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The countermeasures in practice — the prompt and lyrics we used
&lt;/h2&gt;

&lt;p&gt;Here are the actual caption (the ACE-Step prompt) and lyrics we used for a song dealing with the alphabet. The design principles are applied directly, with the grouping changed to avoid the LMNOP problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;caption: children's song, cheerful, playful, simple melody, ukulele,
         glockenspiel, acoustic guitar, clear female vocals, warm, sing-along
duration: 100s / BPM 100 / C major

[Verse]
A - B - C - D
E - F - G
H - I - J - K
L - M - N

[Verse]
O - P - Q
R - S - T
U - V - W
X - Y - Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a seemingly "correct" prompt, with the textbook countermeasures applied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measurement — why the numbers scattered despite the countermeasures
&lt;/h2&gt;

&lt;p&gt;I transcribed all 8 generated songs with mlx-whisper (large-v3-turbo, fully local) and measured the match rate against the lyrics actually used. Each song was generated as 2–4 takes with different seeds under the same prompt, so even the same song with the same prompt shows variation in match rate from take to take.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Song&lt;/th&gt;
&lt;th&gt;Take-to-take match-rate range (max-min, pp)&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01 ABC Song&lt;/td&gt;
&lt;td&gt;58.6&lt;/td&gt;
&lt;td&gt;Enumeration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02 Numbers Song&lt;/td&gt;
&lt;td&gt;12.2&lt;/td&gt;
&lt;td&gt;Enumeration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05 Days &amp;amp; Months&lt;/td&gt;
&lt;td&gt;27.7&lt;/td&gt;
&lt;td&gt;Enumeration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04 Weather&lt;/td&gt;
&lt;td&gt;7.3&lt;/td&gt;
&lt;td&gt;Non-enumeration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06 What Time Is It&lt;/td&gt;
&lt;td&gt;28.8&lt;/td&gt;
&lt;td&gt;Non-enumeration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07 Seasons&lt;/td&gt;
&lt;td&gt;75.2&lt;/td&gt;
&lt;td&gt;Non-enumeration (generation glitch in v1b)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08 Introduce Yourself&lt;/td&gt;
&lt;td&gt;41.0&lt;/td&gt;
&lt;td&gt;Non-enumeration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To be honest, the largest range belongs to the non-enumeration 07 Seasons (75.2pp). But the scoring records explicitly note that this song's v1b (0.0%) is a "defective take where Whisper kept hallucinating 'Thank you'," and that 01 ABC Song's v1b (16.7%) is also a "defective take with a broken chorus loop." In other words, engine-side glitches and take-to-take variation are mixed together, and the size of the range alone does not let you conclude "it broke because it's an enumeration."&lt;/p&gt;

&lt;h2&gt;
  
  
  The main point — prompt fixes were counterproductive for enumeration lyrics only
&lt;/h2&gt;

&lt;p&gt;To separate out the influence of generation glitches, instead of the "range" (which includes defective takes), let's look only at each song's &lt;strong&gt;best take&lt;/strong&gt; (the highest match rate that prompt could produce). For 7 of the 8 songs, we regenerated a "v2" in which the prompt and lyrics were revised to reflect an independent grader's rubric feedback (unnatural English, syllable overruns, lack of expression, etc.). Comparing the v1 best take against the v2 best take yields the following.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F501xagn0r2nh4vqovltj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F501xagn0r2nh4vqovltj.png" alt="A horizontal bar chart comparing the v2-minus-v1 best-take deltas for 3 enumeration songs (01 ABC / 02 Numbers / 05 Days&amp;amp;Months) and 4 non-enumeration songs (04 Weather / 07 Seasons / 06 What Time / 08 Introduce). Enumeration: 01 is -22.1pp, 05 is -10.4pp, only 02 is +1.2pp. Non-enumeration: 08 +7.5pp, 07 +3.5pp, 04 +0.2pp, only 06 is -6.2pp."&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;How the best-take match rate changed before and after the prompt fix (v2). Among enumeration songs (blue) 2 of 3 got worse, while among non-enumeration songs (orange) 3 of 4 improved.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average v1→v2 delta for the 3 enumeration songs (ABC / Numbers / Days&amp;amp;Months): &lt;strong&gt;-10.43pp&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Average delta for the 4 non-enumeration songs (Weather / What Time / Seasons / Introduce — the 4 that were regenerated as v2): &lt;strong&gt;+1.25pp&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rubric grader's feedback was reasonable from the standpoint of "singability," "educational value," and so on (e.g. adding TPR action words to 01 ABC, removing the stray &lt;code&gt;"and"&lt;/code&gt; in 02 Numbers). Even though the lyrics and structure should have improved, only the enumeration songs moved in the direction of being harder to hear. As of this writing (July 2026), ACE-Step 1.5 is a lyrics-to-song model: it renders the entire prompt (including the caption and structure tags) into audio in one shot, so even fixing just a part of the lyrics changes the acoustic realization of the whole generation (the stability of pronunciation) about as much as drawing a different seed would. Because a short string of enumerated symbols has no context to fill it in and is directly exposed to pronunciation wobble, one can hypothesize that this "seed wobble" is especially prone to translating straight into unintelligibility.&lt;/p&gt;

&lt;p&gt;The data in this article is single-batch, in-house data with n=3 enumeration and n=4 non-enumeration, so it cannot be called statistically significant. That said, our own scoring records left, from the same batch, the lesson that "lyric revision (v2) and seed luck are independent — because clarity can drop even when the v2 lyrics are better, the correct approach is to not throw away the old version and to adopt the best take per song." This delta calculation came out consistent with that lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducible takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;For songs containing enumeration-style lyrics (alphabet, numbers, days of the week, month names), prefer taking multiple different-seed takes under the same prompt, scoring them, and picking the best take — over fixing the prompt and rebuilding a single track.&lt;/strong&gt; In this data, rewriting to reflect feedback was more likely than not to backfire.&lt;/li&gt;
&lt;li&gt;At the design stage, put the LMNOP countermeasures (changing the grouping, spelling numbers out in English, &lt;code&gt;A - B - C&lt;/code&gt; separators, syllable counts within ±1–2) into the very first prompt. It works better than fixing things after the fact.&lt;/li&gt;
&lt;li&gt;Don't judge by match rate alone; record generation glitches (hallucinations, broken loops) separately from lyric-structure-derived variation. Jumping to "large range = bad lyrics," as here, would mistakenly turn a defective take like 07 Seasons into evidence for a design principle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The concrete procedure for match-rate scoring with mlx-whisper (including the arm64-build pitfalls under uvx) is explained in the earlier article "&lt;a href="https://zenn.dev/umamon/articles/3a406922aed8e3" rel="noopener noreferrer"&gt;Measuring the quality of local AI composition in numbers — a hands-on record of building a QA pipeline that scores lyric-match rates with mlx-whisper&lt;/a&gt;." This article focused on the design principles and on measuring the effect of regeneration.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>music</category>
      <category>machinelearning</category>
      <category>localllm</category>
    </item>
    <item>
      <title>Automation was dead for 3 days with zero errors — a broken launchd plist, and the "monitor of the monitor" that died too</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 23 Jul 2026 13:40:13 +0000</pubDate>
      <link>https://dev.to/bigkijimon/automation-was-dead-for-3-days-with-zero-errors-a-broken-launchd-plist-and-the-monitor-of-the-1799</link>
      <guid>https://dev.to/bigkijimon/automation-was-dead-for-3-days-with-zero-errors-a-broken-launchd-plist-and-the-monitor-of-the-1799</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://zenn.dev/umamon/articles/blog-automation-diary" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt; (Japanese). Cross-posted here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had automation jobs running as launchd daemons (launchd is macOS's cron equivalent), and one day they quietly stopped — yet not a single error line showed up in any log. This is the record of how, on 2026-07-14, I pinned that state down with real logs and rebuilt the setup so that "no errors" would never be trusted again. It's aimed at people with the exact same configuration: an auto-posting pipeline driven by n8n running as a launchd daemon on macOS (M1 Max, 64GB).&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptom: automation stops with zero errors
&lt;/h2&gt;

&lt;p&gt;The auto-posting workflow (n8n) was supposed to stay resident via launchd's &lt;code&gt;KeepAlive&lt;/code&gt;, but at some point it just stopped running entirely. &lt;code&gt;launchctl&lt;/code&gt; reported no error. The stderr log was empty too. "No trace of the process having crashed" is the nastiest state of all, because from the logs alone you can't tell whether the monitored target "never started" or "started but isn't doing any work."&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause #1: the plist was a JSON-array stub
&lt;/h2&gt;

&lt;p&gt;What was actually broken was a plist file under &lt;code&gt;~/Library/LaunchAgents/&lt;/code&gt;. Looking inside, it was like this (a generalized example for reproduction):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ProgramArguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"n8n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not the valid XML plist that launchd requires — it was a JSON array containing only the contents of &lt;code&gt;ProgramArguments&lt;/code&gt;. When launchd tries to load this, it does not report a syntax error; it &lt;strong&gt;silently does nothing&lt;/strong&gt;. The process never even starts, yet no startup error is logged anywhere — this was the true nature of "automation stops with zero errors."&lt;/p&gt;

&lt;p&gt;This kind of breakage can be detected mechanically with a single command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;plutil &lt;span class="nt"&gt;-lint&lt;/span&gt; ~/Library/LaunchAgents/com.example.myjob.plist
&lt;span class="go"&gt;com.example.myjob.plist: OK

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;plutil &lt;span class="nt"&gt;-lint&lt;/span&gt; ~/Library/LaunchAgents/com.example.broken.plist
&lt;span class="go"&gt;com.example.broken.plist: Unexpected character c (0x7b) at line 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this against every plist on the actual machine, &lt;strong&gt;15&lt;/strong&gt; were invalid at the time (the first-run value of the healthcheck log described below, 2026-07-14). The cause was an organization-wide service-renaming task carried out around the same period: 7 of the plists were converted to proper XML, but the rest — including n8n itself — were left un-fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause #2: the monitor itself was dead for the same reason
&lt;/h2&gt;

&lt;p&gt;Even nastier was that the plist for the health-monitoring job — the very thing meant to watch for "has the automation stopped?" — was broken in exactly the same JSON-array-stub form. In other words, &lt;strong&gt;the watchman had never started in the first place&lt;/strong&gt;. That nobody noticed the stoppage was no coincidence: the type of failure (invalid plist = silent ignore) was the kind that can kill both the monitored target and the monitor at the same time.&lt;/p&gt;

&lt;p&gt;There's another structural cause: location. &lt;code&gt;~/Library/LaunchAgents/&lt;/code&gt; is an OS-level configuration store, and it sits outside the folders you routinely search and take inventory of for documents or code. No matter how well-organized your knowledge management or code search setup is, this location was never on the inspection path to begin with. The only way to notice "something that's supposed to be running actually isn't" was to have a separate job that actively goes and looks at this location itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fax6g22xvahieivynwebq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fax6g22xvahieivynwebq.png" alt="Trend of the number of anomalous plists in the healthcheck log. 15 on the first run, flat at 14 across seven runs, and 0 on the most recent run." width="800" height="379"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Trend of anomalous-plist counts across 10 healthcheck runs. First run 15 → flat at 14 for seven runs (the deferred-response portion) → 0 on the most recent run.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Permanent fix: build a setup that doesn't trust "no errors"
&lt;/h2&gt;

&lt;p&gt;After rewriting the two broken ones (n8n itself and the health monitor itself) as proper XML plists and recovering them, I set up a single script — resident via launchd, running every morning at a fixed time — that mechanically does only these three things, so the same failure would never slip past again.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;plutil -lint&lt;/code&gt; against all of &lt;code&gt;~/Library/LaunchAgents/*.plist&lt;/code&gt; and judge validity.&lt;/li&gt;
&lt;li&gt;Cross-check against the output of &lt;code&gt;launchctl list&lt;/code&gt; to also catch ones that are valid but not loaded.&lt;/li&gt;
&lt;li&gt;Reflect the results into an internal ledger file by fully rewriting it every time, and send a notification &lt;strong&gt;only when there is at least one anomaly&lt;/strong&gt; (on days with no anomalies, send nothing — so as not to cause notification fatigue).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running the same check against the actual machine as I write this article, the result is as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; ~/Library/LaunchAgents/&lt;span class="k"&gt;*&lt;/span&gt;.plist | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="go"&gt;13

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; ~/Library/LaunchAgents/&lt;span class="k"&gt;*&lt;/span&gt;.plist&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;plutil &lt;span class="nt"&gt;-lint&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;": OK"&lt;/span&gt;
&lt;span class="go"&gt;13

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; ~/Library/Logs/.../healthcheck.log
&lt;span class="go"&gt;healthcheck done: anomalies=0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 13 currently existing plists are all valid (&lt;code&gt;plutil -lint&lt;/code&gt; is OK for 13/13). The healthcheck log has 10 runs recorded, and the anomaly count traces this path: first run 15 → seven consecutive runs at 14 (the period when the deferred-response portion remained) → 0 on the most recent run. Rather than an average, the truest picture comes from the three-point set: both ends (15 and 0) plus the fact that it stalled at 14 for seven runs in between. It wasn't "it'll fix itself eventually" — it dropped to 0 all at once on the run where processing of the deferred portion finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adjacent traps: TCC and the Node.js version
&lt;/h2&gt;

&lt;p&gt;This configuration had two more pitfalls specific to launchd-based automation.&lt;/p&gt;

&lt;p&gt;The first is macOS's TCC (the privacy-protection access control). When a child process launched from launchd tries to read a protected folder under &lt;code&gt;~/Documents&lt;/code&gt;, the Full Disk Access you supposedly granted from the GUI may not be inherited, and it gets rejected with &lt;code&gt;Operation not permitted&lt;/code&gt;. You need to grant Full Disk Access to the executable binary that launchd uses (in this case, the node binary itself); even inserting a shell wrapper does not make the child process inherit the permission.&lt;/p&gt;

&lt;p&gt;The second is a trap rooted in the Node.js major version. Dependencies that include native addons (in this case &lt;code&gt;isolated-vm&lt;/code&gt;) can build or fail to build depending on the Node.js version. In my environment, Node 26.x failed like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;serializer_nortti.o Error 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switching to Node 22.x (keg-only, no interference with the default Node 26) let the build pass. It wasn't "upgraded to the latest and it stopped working" but "on the latest version it won't even build in the first place" — the kind of defect where the error message alone makes it hard to isolate which layer (TCC / launchd / Node.js) is at fault.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusable lessons
&lt;/h2&gt;

&lt;p&gt;If you're running the same setup (launchd daemons + periodically executed automation), the following four make a checklist you can reuse as-is.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Right now&lt;/strong&gt;, run &lt;code&gt;plutil -lint&lt;/code&gt; against all your plists. If even one is invalid, it's dying silently without emitting any error.&lt;/li&gt;
&lt;li&gt;Make sure the liveness of the health monitor itself can be confirmed through a path separate from the monitored target (a separate process, a separate log). A design where the monitor dies together with its target under the same failure mode does not function as a monitor.&lt;/li&gt;
&lt;li&gt;OS-level configuration stores (launchd plists, cron tables, systemd unit files, etc.) are often outside the folders your everyday code and document searches cover. The more a location is off the inspection path, the more you need a separate job that actively goes and looks at it.&lt;/li&gt;
&lt;li&gt;Don't base the evidence of "it's running" on "no errors are showing up." Base it on "the last success timestamp is recent" (the tail of a log, or the timestamp of a heartbeat file). The case of "never even started" and the case of "waiting normally, doing nothing" cannot be distinguished by error logs alone.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>macos</category>
      <category>automation</category>
      <category>devops</category>
      <category>node</category>
    </item>
    <item>
      <title>I cut subagents from 11 to 0 and cost dropped 87.7% — even though total tokens went UP 27.7%</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Tue, 21 Jul 2026 00:38:47 +0000</pubDate>
      <link>https://dev.to/bigkijimon/i-cut-subagents-from-11-to-0-and-cost-dropped-877-even-though-total-tokens-went-up-277-4le8</link>
      <guid>https://dev.to/bigkijimon/i-cut-subagents-from-11-to-0-and-cost-dropped-877-even-though-total-tokens-went-up-277-4le8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is an English version of an article I originally published (in Japanese) on &lt;a href="https://zenn.dev/umamon/articles/f2e2b8f78bc25d" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Split the work across role-specific agents and both quality and cost will improve" — that was the assumption behind the automated tech-blog pipeline I built. The measurements said the opposite. When I cut subagents &lt;strong&gt;from 11 to zero&lt;/strong&gt;, total token usage actually went &lt;strong&gt;up 27.7%&lt;/strong&gt;, yet the cost of producing one article dropped &lt;strong&gt;87.7%&lt;/strong&gt;. This is a hands-on record of a person who writes no code building this pipeline with nothing but Claude Code prompts, and then flipping the design based on measured numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: I assumed more agents was better
&lt;/h2&gt;

&lt;p&gt;This pipeline is an in-house system I built — purely with Claude Code prompts — to generate multiple blogs unattended. Not a single line was written by a programmer. I build reservation systems, games, and menu-bar apps the same way.&lt;/p&gt;

&lt;p&gt;The first version of the tech-blog pipeline was a &lt;strong&gt;7-agent setup&lt;/strong&gt;: a chief coordinating a set of specialists.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;chief (orchestration)&lt;/li&gt;
&lt;li&gt;keyword (topic selection)&lt;/li&gt;
&lt;li&gt;research (primary-source research)&lt;/li&gt;
&lt;li&gt;outline (structure)&lt;/li&gt;
&lt;li&gt;writer (drafting)&lt;/li&gt;
&lt;li&gt;publish (formatting for submission)&lt;/li&gt;
&lt;li&gt;qa (scoring — an independent process)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It felt natural. Human teams get more specialized when they divide roles; surely splitting "topic picker" and "writer" lets each focus on its job. That was the theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement: where the $41.7 per article was actually going
&lt;/h2&gt;

&lt;p&gt;I counted the token breakdown of one article that actually ran to completion under this 7-agent setup (a hands-on piece about local image generation, ~4,000 characters of body, 2 diagrams, published 2026-07-14). Here is what came out.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Segment&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parent session&lt;/td&gt;
&lt;td&gt;737,594&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11 subagents (total)&lt;/td&gt;
&lt;td&gt;6,216,660&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6,954,254&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The final output (the article body) was only about 4,000 tokens. In other words, &lt;strong&gt;total tokens were roughly 1,738× the deliverable&lt;/strong&gt;. And breaking it down further, 79% of the billed tokens (5,455,523) were "cache re-reads" — the same shared context (system prompt, skill definitions, role definitions) being re-read every time execution crossed an agent boundary.&lt;/p&gt;

&lt;p&gt;The decisive thing was a design premise. Each stage ran "one at a time, in order." In other words, &lt;strong&gt;there was no parallel debate and no cross-checking&lt;/strong&gt;. Splitting into 7 bodies didn't change the fact that the same model was making the calls on the same source material. The split produced cost and nothing else; it contributed nothing to the quality of the judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result: tokens up 27.7%, yet cost down 87.7%
&lt;/h2&gt;

&lt;p&gt;So I scrapped the 7-agent setup (the sister pipeline used 9 agents) and consolidated the six stages — ① topic → ② primary sources → ③ outline → ④ drafting → ⑤ QA → ⑥ submission — into &lt;strong&gt;a single session that executes them top-to-bottom as "steps."&lt;/strong&gt; Only QA stayed a separate process (more on that below). The chart below is the measured comparison of the same article run to completion under the old vs. new design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcn4sdcjyo4t51p5twy2h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcn4sdcjyo4t51p5twy2h.png" alt="A diverging bar chart showing that consolidating subagents from 11 to 0 changed total tokens +27.7%, cache writes -76.1%, cache reads +55.0%, and cost -87.7%." width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One article, completed-run vs. completed-run (from an internal measurement record, 2026-07-17).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At a glance the numbers look contradictory. Total tokens went 6,954,254 → 8,880,984, &lt;strong&gt;up 27.7%&lt;/strong&gt;. Cache reads also went 5,455,523 → 8,457,896, &lt;strong&gt;up 55.0%&lt;/strong&gt; (with the whole workflow's context riding in one session, reads actually accumulate). And yet cost went $41.68 → $5.12, &lt;strong&gt;down 87.7%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reason is the price gap between cache "writes" and cache "reads." Every time execution crosses an agent boundary, a fresh context re-prefill (a cache write) occurs — and that went 1,402,348 → 334,689 tokens, &lt;strong&gt;down 76.1%&lt;/strong&gt;. From our internal operational logs, a write worked out to be &lt;strong&gt;roughly 12.5× more expensive&lt;/strong&gt; than a re-read ($3.75/M vs $0.30/M — an internal estimate, not Anthropic's official price table). No matter how much reads increase, if writes drop 76%, the net comes out ahead.&lt;/p&gt;

&lt;p&gt;Why is a "write" so expensive? Prompt caching prefills the shared preamble (system prompt, skill definitions, role definitions) once, loads it into the KV cache, and from then on only "reads" it. A read is cheap because it reuses an already-cached prefix. But when you spawn a subagent, the parent's KV cache is not inherited as-is. The child has to load that same shared preamble again and "write" it back into the cache — that is the re-prefill, and it's why cache writes are billed at a premium. &lt;strong&gt;The high re-write piled up once per agent boundary.&lt;/strong&gt; Consolidation cuts the number of those re-writes directly.&lt;/p&gt;

&lt;p&gt;That said, to be honest, not all of that cost gap is the effect of consolidation. Our internal read is that &lt;strong&gt;consolidation itself accounted for about 40%, and the remaining ~60% was model choice (Opus → Sonnet)&lt;/strong&gt;. This isn't "delete the agents and it magically got cheap"; it's a combination of "cut the re-prefill cost at agent boundaries, and also revisit the model."&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing I kept split anyway
&lt;/h2&gt;

&lt;p&gt;QA is the one stage I did not consolidate; I kept it as a separate process (independent scoring by a cheap model). The reason is simple: &lt;strong&gt;when you let the generating model score its own output, the evaluation gets pulled toward the convenience of generation.&lt;/strong&gt; It's the question of whether you can honestly give your own article a 75. I treat this as one of the few exceptions worth the cost of splitting.&lt;/p&gt;

&lt;p&gt;The general rule you can extract from this comes down to one sentence: &lt;strong&gt;"Is this stage genuinely debating and cross-checking in parallel, or is it just going around one at a time?"&lt;/strong&gt; If the latter, the agent boundary is decoration that only generates prefill cost. If the former (a stage that needs an independent set of eyes), it's worth paying the cost to split.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reproducible takeaway
&lt;/h2&gt;

&lt;p&gt;If your pipeline "splits agents by role," first check whether that stage really makes parallel, independent judgments. If it doesn't, the agent boundary is just an expensive file hand-off that generates a context re-prefill. &lt;strong&gt;In a design where each stage runs serially, waiting its turn, splitting is nothing but cost.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sort each stage with these two questions and the call comes fast:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does this stage race multiple candidates in parallel?&lt;/strong&gt; — If No, the split isn't producing parallelism; only the boundary's re-prefill cost remains. Consolidate it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does this stage need an independent verifier (a set of eyes whose interests differ from the generator's)?&lt;/strong&gt; — If Yes, it's worth paying the cost to split, like QA.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stages that are No on both: consolidate. Only stages that are Yes on either one keep a boundary. Conversely, if stages genuinely run concurrently and branch dynamically on each other's intermediate results — racing multiple candidates at once, switching the next stage based on partial results — then the boundary is doing more than prefill work. &lt;strong&gt;Consolidate stages that merely hand off one at a time, and split only the stages that truly need concurrency, independence, or dynamic branching.&lt;/strong&gt; That's where the line is.&lt;/p&gt;

&lt;p&gt;In fact, this very article is a record of that initial intuition — "more agents makes it smarter" — being overturned by measurement. Splitting is not free.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>ai</category>
      <category>claude</category>
    </item>
    <item>
      <title>How many tok/s does a local LLM get on an M1 Max 64GB — I measured four models and the bigger one won</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Sun, 19 Jul 2026 04:46:16 +0000</pubDate>
      <link>https://dev.to/bigkijimon/m1-max-64gbderokarullmhahe-tokschu-ruka-shou-chi-ti4moderuwoshi-ce-sitaradekaifang-gasu-i-ni-zhuan-gachu-ta-330p</link>
      <guid>https://dev.to/bigkijimon/m1-max-64gbderokarullmhahe-tokschu-ruka-shou-chi-ti4moderuwoshi-ce-sitaradekaifang-gasu-i-ni-zhuan-gachu-ta-330p</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://zenn.dev/umamon/articles/7cc1abcb4b4a82" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt; (Japanese). Cross-posted here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;How many tokens per second does a local LLM actually get on a four-year-old M1 Max (64GB) today? When I went looking for benchmark sites, they were all full of M4/M5-generation numbers — the M1 Max row was simply missing. If it doesn't exist, you have to measure it yourself, so I benchmarked four models I had on hand under identical conditions. The table that came out had one counter-intuitive line in it: &lt;strong&gt;a model that is 3.6× larger on disk runs 1.5× faster than a smaller one.&lt;/strong&gt; This article is that measurement, and why it happens.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every number here is measured on my own machine (M1 Max 64GB / Ollama 0.30.8). Each figure is the median of three runs — no guesses, no borrowing numbers from other machines. Where I couldn't measure something, I say so.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The bottom line: measured tok/s for four models on hand
&lt;/h2&gt;

&lt;p&gt;The measurement conditions are the same for every model. I sent the same Japanese prompt to &lt;code&gt;/api/generate&lt;/code&gt; (&lt;code&gt;num_predict=300&lt;/code&gt;, &lt;code&gt;temperature=0.7&lt;/code&gt;, &lt;code&gt;think=false&lt;/code&gt;) and computed decode (generation) tok/s from the response's &lt;code&gt;eval_count ÷ eval_duration&lt;/code&gt;. I took the median of three runs each. Before measuring, I confirmed GPU utilization was 0% with no other generation jobs running.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Arch&lt;/th&gt;
&lt;th&gt;Total params&lt;/th&gt;
&lt;th&gt;active/token&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;decode tok/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MoE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;36.0B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~3B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;23.9GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60.4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.6-uncensored-cc&lt;/td&gt;
&lt;td&gt;MoE&lt;/td&gt;
&lt;td&gt;34.7B&lt;/td&gt;
&lt;td&gt;~3B&lt;/td&gt;
&lt;td&gt;21.2GB&lt;/td&gt;
&lt;td&gt;59.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma4&lt;/td&gt;
&lt;td&gt;dense&lt;/td&gt;
&lt;td&gt;8.0B&lt;/td&gt;
&lt;td&gt;8.0B&lt;/td&gt;
&lt;td&gt;9.6GB&lt;/td&gt;
&lt;td&gt;56.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.5&lt;/td&gt;
&lt;td&gt;dense&lt;/td&gt;
&lt;td&gt;9.7B&lt;/td&gt;
&lt;td&gt;9.7B&lt;/td&gt;
&lt;td&gt;6.6GB&lt;/td&gt;
&lt;td&gt;40.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three runs were, in order: qwen3.6 at 60.4/60.1/60.5, uncensored at 59.5/59.6/59.1, gemma4 at 56.6/56.5/56.6, qwen3.5 at 40.6/40.5/40.5. &lt;strong&gt;The spread is tiny in every case — 0.1 to 0.5 tok/s.&lt;/strong&gt; These are medians, not single shots, so they're trustworthy. All are Q4_K_M quantization.&lt;/p&gt;

&lt;p&gt;Reading the table top to bottom doesn't parse naively. &lt;strong&gt;The largest model on disk, qwen3.6 (23.9GB), is the fastest (60.4), and the smallest, qwen3.5 (6.6GB), is the slowest (40.5).&lt;/strong&gt; On-disk size and speed run in reverse order. That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the bigger one is faster: decode is governed by active params
&lt;/h2&gt;

&lt;p&gt;An LLM's decode speed (the rate at which it emits tokens one at a time) is roughly "&lt;strong&gt;memory bandwidth ÷ the bytes of parameters actually read per token&lt;/strong&gt;." On the same machine — same memory bandwidth — the fewer parameters you touch per token, the faster you go.&lt;/p&gt;

&lt;p&gt;This is where the MoE (Mixture of Experts) vs. dense distinction matters. &lt;code&gt;ollama show&lt;/code&gt; makes the architectures clearly different.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;qwen3.6&lt;/strong&gt;: &lt;code&gt;qwen35moe&lt;/code&gt;, with &lt;code&gt;expert_count=256&lt;/code&gt; and &lt;code&gt;expert_used_count=8&lt;/code&gt;. It has 36B total parameters, but to produce one token &lt;strong&gt;it uses only 8 of the 256 experts&lt;/strong&gt;. The parameters actually in motion are about 3B.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;qwen3.5&lt;/strong&gt;: &lt;code&gt;qwen35&lt;/code&gt; (dense, no expert keys). It uses all 9.7B every token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gemma4&lt;/strong&gt;: &lt;code&gt;gemma4&lt;/code&gt;, dense 8.0B. Also uses 8B every token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the "real work per token" is about 3B for qwen3.6, 8B for gemma4, and 9.7B for qwen3.5. &lt;strong&gt;The order of least work is exactly the order of fastest speed&lt;/strong&gt; — and the measurements lined up that way. qwen3.6 is faster than qwen3.5 while carrying 3.6× the disk size because those 24GB of weights sit in storage (or rather in unified memory), but only a fraction of them (~3B) is read out per token.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fimnpqsxppyuzkomj5nve.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fimnpqsxppyuzkomj5nve.png" alt="Horizontal bar chart comparing decode tok/s for four models. From top: qwen3.6 (60.4, MoE 24GB), qwen3.6-unc (59.5, MoE 21GB), gemma4 (56.6, dense 9.6GB), qwen3.5 (40.5, dense 6.6GB). Ordering by disk size does not match the speed ordering." width="800" height="448"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Even ordered by disk size (largest at top), the order does not match decode speed. What determines speed is not total size but active params per token. The MoE qwen3.6 is "big but fast."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Dense vs. dense follows size, as expected
&lt;/h2&gt;

&lt;p&gt;Set MoE aside and the two dense models line up by the textbook. gemma4 (8.0B) does 56.6, qwen3.5 (9.7B) does 40.5. &lt;strong&gt;The smaller dense model is faster.&lt;/strong&gt; 8.0B vs. 9.7B is about a 1.2× ratio, but the measurement opened up to 56.6 vs. 40.5 — about 1.4×. The gap being larger than the parameter ratio is because per-model differences (attention implementation, KV-cache handling, etc.) ride on top; here I'll only say "roughly ordered by size." I make no claim of strict proportionality.&lt;/p&gt;

&lt;p&gt;Put another way: "&lt;strong&gt;I want a lighter model = pick the one with the smaller disk size" is only correct within dense models.&lt;/strong&gt; The moment you put an MoE in the running, that intuition collapses. qwen3.6 eats 3.6× the disk of qwen3.5 yet beats it on speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefill (prompt reading) is a separate story
&lt;/h2&gt;

&lt;p&gt;Everything above is decode (output) speed. Prefill (prompt eval), which reads the input, behaves differently, so I measured it separately.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;prefill tok/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gemma4&lt;/td&gt;
&lt;td&gt;639&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.6-uncensored&lt;/td&gt;
&lt;td&gt;613&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.6&lt;/td&gt;
&lt;td&gt;543&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.5&lt;/td&gt;
&lt;td&gt;394&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prefill runs at roughly 10× decode speed (input tokens can be processed in parallel, batched). Here's one measurement pitfall, told honestly. When I first measured qwen3.6's prefill, I got &lt;strong&gt;an anomalous 4876 tok/s — about 8× the others.&lt;/strong&gt; Obviously wrong. The cause: I had just run decode on the same model, so part of the prompt was still in cache and got treated as "already read." Re-measuring with a unique prompt settled it at 543 tok/s. &lt;strong&gt;Caching makes speed measurements lie, easily.&lt;/strong&gt; So prefill should always be measured with fresh input (the table above is the re-measured value).&lt;/p&gt;

&lt;h2&gt;
  
  
  About the M1 Max "blank space"
&lt;/h2&gt;

&lt;p&gt;The whole reason I started measuring was that searching benchmark sites turned up no M1 Max numbers. The tok/s tables for new models are usually filled with M4 Ultra / M5 Max / M4 Max, and the row for the 2021 M1 Max is blank, or at best a single value of dubious provenance. For someone weighing a used 64GB M1 Max, that's effectively nothing to go on.&lt;/p&gt;

&lt;p&gt;These numbers are just a single point of measurement — one machine, M1 Max 64GB, Q4_K_M, Ollama 0.30.8 — and I'm not generalizing. Even so, the one point that "&lt;strong&gt;even on a four-year-old M1 Max, an active-3B MoE gets 60 tok/s, and 8B-class dense gets 40–57 tok/s&lt;/strong&gt;" should be a far better clue than a blank. Subjectively, 60 tok/s streams output faster than you can read, and even at 40 tok/s there's almost no sense of waiting. It's practical for local use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducible takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decode speed is governed by active params/token, not disk size.&lt;/strong&gt; If you're picking a model and think "small = fast," first check whether it's MoE or dense. An MoE can be "big but fast."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for cache in speed measurements.&lt;/strong&gt; Prefill in particular, if you fire the same prompt repeatedly, gives absurdly fast values from the previous run's cache. Measure with unique input every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the median of three runs.&lt;/strong&gt; A single shot can catch an outlier. Here the spread stayed within 0.1–0.5, so I trust these four values.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your environment (model, Ollama/MLX version, quantization) differs, the numbers will change. If anyone is running other models on an M1 Max, I'd love to know your measured values — especially MoE models (beyond qwen3.6) and tok/s at different quantizations. There simply isn't enough M1-Max-generation data out there yet.&lt;/p&gt;

</description>
      <category>llm</category>
    </item>
  </channel>
</rss>
