Skip to content

flashCat 1.5.4:新增 flashcat 命令行与本地控制接口,概览指标改为按请求实测

Choose a tag to compare

@imi4u36d imi4u36d released this 11 Nov 07:00
· 11 commits to main since this release
8656241

flashCat 1.5.4

这一版把 CLI 与本地控制接口补上:flashcat 能列出/启停引擎、切模型、流式对话、
读日志与网关状态,应用没在跑时也能做离线操作。概览的取数口径同步重做:预填充
与解码优先读引擎按请求自报的 timings,量不出来就画「—」;第四格从内存压力
换成按 token 加权的缓存命中率。

前缀缓存改为落盘,重启应用不用从零预填充;切走对话不再中断生成,回到列表能点
回正在跑的那一段;应用内不再注册任何快捷键,键盘效率交给终端。

新增

命令行与本机控制接口

  • flashcat 命令行。 随 .app 分发在 Contents/Helpers/flashcat,.pkg
    安装时链接到 /usr/local/bin。支持 status、engines(list/start/stop/
    restart/use/install/uninstall/port/logs)、models(list/use/download/delete/
    path)、chat(stdin、流式、思考过程、JSON)、gateway、logs(级别过滤与
    --follow)、config、app、doctor、completion、cli。
  • 应用侧本机控制接口。 默认 127.0.0.1:9100,端口被占用时自动顺延;每次
    启动生成随机令牌,control.json 以 0600 保存。设置页新增「命令行接口」开关
    与端口。接口与推理网关分离,网关关闭时仍可驱动应用。
  • 共享路径与协议。 FlashCatPaths、EngineInstallations 与
    FlashCatKit/Control/ControlProtocol.swift 收进 Kit,应用和 CLI 使用同一套
    支持目录、模型目录、引擎发现规则与控制文档。
  • 命令行脚本契约。 结果型命令都支持 --json;结果走 stdout、进度走 stderr;
    固定退出码(应用未运行 3、找不到 4、超时 5、冲突 6、令牌 7);
    zsh / bash / fish 补全;--timeout、--quiet、--no-color。
  • 离线能力。 应用未运行时仍可 status、列出引擎与磁盘模型、安装/卸载引擎、
    读日志与读取/修改纯配置;需要应用的动作明确报错,不伪造成功。

前缀缓存落盘

  • 重启后不再从零预填充。 引擎的 hot prefix cache 只在内存里,App 一重启就
    清零,上一次会话的整段历史只能重算——同一段 1.5 万 token 的会话,带缓存
    恢复是 0.1 秒,重启后无缓存是 13.8 秒,十万 token 级能到 3 分钟。四家引擎
    本来都有这条 SSD 层,只是没人打开:Splash 的 --persistent-cache、oMLX 的
    --paged-ssd-cache-dir(不传这个目录它的前缀缓存等于全关)、MLX-Serve 的
    --prefix-cache-disk 默认都是关的,MTPLX 默认开着但恢复被自己的
    boundary 预算挡掉。现在引擎配置页有「前缀缓存」卡片:开关 + 磁盘预算
    (默认 20 GB),关闭后显式下发各家的 off 参数而不是留空。
  • 统一缓存根。 各家的落盘快照都收到
    Application Support/flashCat/prefix-cache/<engine>/ 下,容量、清理和备份
    只需要管一个目录。MLX-Serve 的路径写死在 $HOME/.mlx-serve/kv-cache、没有
    对应开关,用符号链接指进来。
  • 模型搬家不再作废缓存。 缓存条目按模型的绝对路径做键,所以
    ModelStorageMigration 把权重从 ~/.mtplx/models 搬进应用目录时,该模型
    已有的快照全部失配——实测 23 个条目、约 29 GB 从此再也命中不了,
    同时同一个模型在新路径下有 0 命中。迁移现在改写 manifest 里的 model_path
    而不是放任它变孤儿,并把 ~/.mtplx/session-bank 一并搬到新根下。
  • 测试:PrefixCacheTuningTests 钉住每家的参数拼写(Splash 只认 20G,
    其余认 20GB,写错直接被拒绝)与「0 是关闭、不是回退默认」。

变更

  • 标题栏去掉「命令」按钮。 胶囊形入口不再占用标题栏宽度;命令面板仍可从
    「页面 → 命令面板」菜单打开,行为与之前一致。
  • 每个指标格只有一个 owner。 取数口径收进 AppState 的 owner 表
    (decodeReading / averageDecodeReading / prefillReading,输出
    MetricReading/MetricOrigin),曲线、「最近请求」、灵动岛、菜单栏从
    同一组函数取数,数值与角标同源。分工按物理边界:当前速度 owner = 网关
    (引擎只给聚合口径,五家还各不相同);预填充、TTFT 分布、显存、草稿接受
    率、队列、上下文占用、生命周期阶段 owner = 引擎(网关量不到)。引擎
    .live 只在网关没有会话在跑(客户端直连引擎端口)时兜底;网关读数超过
    两拍未刷新同样降级——「过去的现在」不配冒充当前速度。
  • 概览第四格从「内存压力」改为「缓存命中率」。 命中量 / 提示词总量由网关
    按请求打点累加,按 token 加权而不是逐请求平均(一段 100K 上下文命中 95%
    比十次 1K 冷请求更能代表缓存效率);只统计真的收到 usage 的请求,窗口是
    会话表最近 ≤32 条,换引擎 / 换模型 / 引擎重启后从「—」重新等样本。客户端
    绕过网关直连引擎端口时,退回引擎自报的开机以来比值(mlx-serve / MTPLX 的
    cached / prompt 计数器,oMLX 的 total_cached_tokens / total_prompt_tokens)。
    角标只有 AVG;0% 是合法读数(有样本、一次都没命中),「没样本」才画「—」。
    内存占用仍在菜单栏的读数里。
  • hero 只显示模型启动以来平均。 大数字改读网关按会话累计的解码均速:
    分子是生成 token,分母是首 token 到末 token 的解码时长(不含 TTFT、排队、
    空闲),换引擎 / 换模型 / 引擎重启时清零。曲线继续画实时值,两者回答的
    不再混为一谈;没有累计样本时画「—」。
  • 取消 LAST 采集入口。 解码与预填充的当前速度只认 LIVE;预填充保留
    引擎真的累计过的 AVG(文案写明「开机以来均值」),空闲时画「—」,不再把
    上一请求的实测值挂成当前读数。角标只剩「网关」/「引擎实时」与 AVG。
  • 应用内对话的直播条继续走低通。 DecodeRateMeter(τ=2.5s)服务逐分
    片回调的对话页——那边要的是「最后一个脉冲的强度」;概览只在 1 秒节拍上
    读网关,分片不再顺带刷新概览。
  • 模型广场不再展示推荐引擎标签。 结果卡与详情页去掉那枚强调色的引擎名
    (含「无推荐」);量化、已在本机、内存下限、许可与 Hub 标签照旧保留,权重
    归属仍由下载条里的「默认授权给 …」交代。
  • 对话输入框回车发送、⌘回车换行。 SwiftUI 的 TextField(axis: .vertical)
    只会回车换行、没有换行组合键可留,输入框换成 NSTextView 包装
    (UI/ChatComposerField.swift):回车发送、⌘回车换行,输入法组字期间不误发;
    仍按内容长高、封顶 6 行后滚动,聚焦上抬一截的动效与点击聚焦不变。这是应用里
    唯一的按键判定——属于文本编辑而不是全局快捷键;testReturnKeyIsOnlyHandledByComposer
    把 keyDown 覆盖钉在这一个文件里,UI-SPEC「键盘策略」与发送按钮提示同步写明。
  • 应用内取消所有快捷键。 菜单不再带按键等价物;命令面板只响应鼠标点击,
    不再接管方向键、回车或 esc;对话输入框回车换行、发送改为点击按钮;搜索框
    不再回车提交;设置页端口改为点击「应用」;弹窗去掉 .defaultAction。
    设置页与 README 的快捷键说明同步删除。
  • 设置页新增「命令行接口」卡片。 显示开关、控制端口与 flashcat --help
    的复制入口;「恢复默认」会把控制接口恢复为开启、端口 9100。
  • 发版产物。 scripts/build-native-app.sh 同时编译并嵌入 flashcat;
    make-installer.sh 的 .pkg postinstall 创建 /usr/local/bin/flashcat;
    install-app.sh 在 /usr/local/bin 可写时同样链接;make uninstall 会清掉
    指向已安装应用的链接。

修复

  • 切走对话会把正在生成的回答弄丢。 以前切去概览、新建对话或从列表点开
    另一段,都会直接 cancelGeneration(),而它只写 conversation.json——
    「最近对话」里留着的还是上一轮快照,正在流的那条回答既没了、生成也停了。
    现在切走时把整段对话连同它自己的 threadID 搬到后台继续跑,跑完按自己的
    id 落回历史并提示「已在后台生成完成」;开始生成时先把问题写进最近对话,
    列表里立刻有一行可点,点回来就是把后台那份接回前台接着写。侧栏给正在生成
    的那一行标「生成中」;删除或清空对话会先停掉后台那一轮,别让它跑完再写回来。
  • 切页回来对话区是空的。 ChatPage 重建时 ScrollView 报 0 高度,
    scrollTo 按 0 视口算偏移,等高度补齐就落到内容之外。现在先挂一笔「欠一次
    落底」,由 onScrollGeometryChange 量到真实高度再补;超大回答首次布局只量到
    一部分高度,150 ms 后再补一次,读者已经往上翻时不抢回位置。
  • 设置页改「默认模型」不再换掉正在跑的引擎。 这一栏的菜单按引擎分组,
    选别的引擎的模型以前会直接走一次引擎切换:停掉当前引擎、等待进程退出、
    再加载新权重。现在它只是启动偏好:选择记到那台引擎的配置里,当前引擎
    继续服务,行内标注「下次启动」;只有显式切换引擎(或下次启动)才会让默认
    跟着动。引擎存储里的 default 与 active 因此分开,后台的安装/卸载重建
    也不会再把 activeID 挪到默认引擎上。
  • 下载中心的删除按钮发糊。 危险色原来交给玻璃 tint
    (Native.red.opacity(0.55)):tint 是层半透明色雾,红色图标压在同一片红雾
    上,截图取样底色 rgb(185,102,93)、图标 rgb(244,107,94),对比度只有约
    1.4∶1——看上去像失焦。现在危险态不再染玻璃,改成中性玻璃 + 自绘的 12% 红底
    与 0.8 pt 发丝边,图标用满饱和的 Console.danger;边缘由我们画,不经过玻璃
    模糊,深浅色下都清晰。
  • 「最近请求」报出 37 769 tok/s 这种数。 非流式响应(agent 客户端、
    stream: false 的调用)整段正文一口气回来,网关手里只有一个到达点;
    「全程平均」却拿到达窗口的下限 0.35 秒当分母,14 155 token 的回答于是被摊成
    40 443 tok/s,同一批数字还混进了 hero 的「模型启动以来平均」。现在按请求先读
    引擎在响应里自报的 timings(prompt_n / cached_n / prompt_ms 与
    predicted_n / predicted_ms,mlx-serve 在流式末片和非流式正文里都带),
    分子分母都只取引擎真正前向/生成的那一段;没有 timings 的引擎只要没观测到
    真实的到达跨度就画「—」,不再拿窗口下限编一个。单点会话同样不再进累计均值。
  • 预填充速度始终为空(换到 mlx-serve 后又出现)。 根因是这格只认引擎自报的
    预填充字段:MTPLX 在 in_flight[].prefill_state / latest.prefill_tok_s 里给,
    但几百 token 的预填充只要 0.2~0.4 秒,1 Hz 轮询几乎抓不到 LIVE;mlx-serve
    干脆不报预填充速率,于是卡片又整格变空。现在同样以逐请求的实测值为准:
    引擎给了 timings 就用 (prompt_n − cached_n) / prompt_ms(非流式也有),
    没给才退回网关自己的口径——分子是 usage 里的提示词 token 减去
    cached_tokens,分母是请求发出到首 token 的墙钟,且只对流式响应成立。
    前向不足 128 token 的样本一概不报:命中缓存后剩几十个 token 时,耗时由缓存
    恢复的固定开销主导(实测 2195 个 token 里缓存 2164,31 个前向摊 92ms → 334
    tok/s),那个数字既不是算力也不是缓存速度。预填充进行中引擎的 .live 仍然
    优先,其余时间标 LAST(「网关 · 最近一次预填充实测」),引擎的 LAST/AVG
    只在客户端绕过网关直连引擎端口时兜底。AI 服务自己也会报进程级均值,
    prometheusJSON 现在把 generation_tokens_total / decode_time_seconds.sum
    与 prefill_tokens_total / prefill_time_seconds.sum 读成 .lifetimeMean,
    网关一个样本都没有时 hero 与预填充格也能说出「开机以来多快」。
  • Claude 的流式请求只回空正文。 Anthropic 方言翻译出来的上游请求体没有带
    stream: true(只有调用方拿到的 stream 标志),引擎于是按非流式回一整块
    JSON,而转发层是按 data: 行解析 SSE 的——一行都认不出来。Claude Code 这类
    客户端只会收到 message_start / ping / message_stop,正文全空,概览里
    这一路连一个 token 都没有。现在翻译层把 stream 写进上游请求体,流式时再带
    stream_options.include_usage,末片的 usage 才拿得到 completion_tokens 与
    缓存命中量。
  • 组字中的输入被自动清掉。 输入框原来拿 textView.string 直接跟草稿
    Binding 比:输入法组字的 marked text 已经在文本视图里、却还没进 Binding,
    任何一次视图刷新(每秒指标、焦点变化)一比就把没确认的拼音回写成空串。现在
    只在「视图没有未回报的编辑」且「草稿确实被外部改写」时才回写,组字期间一律
    不碰,等提交后由 textDidChange 对齐;也不再由草稿状态反向抢第一响应者——
    组字中途被夺焦同样会丢候选。同一份源码跑了对照组:旧逻辑属性一更新就是
    marked=false 视图="",新逻辑保持 marked=true 视图="zhong" 直到提交。
  • 背景的透明度与模糊「拖一个条,另一个条的改动才生效」。 窗口表面
    (isOpaque / 底色)和毛玻璃层的强度都只在 DesktopBackdrop.updateNSView
    里更新,而这个 representable 的入参只有 blur:只拖 APP 透明度时它一次
    都不会被回调,窗口仍是不透明的,桌面根本不露出来;等到下次拖模糊,才连带
    把上一步的透明度一起落下去。现在改由 BackgroundSettings 的 didSet 直接
    驱动 BackgroundSurface,参数一变窗口和毛玻璃层立刻跟上,不再依赖任何视图
    的更新时机;透明度为 0、又没有背景图时把模糊滑杆禁用并说明原因,不再留一根
    按了没反应的滑杆。
  • 图片模式的透明度后半程没有变化。 保底渐变原来写死
    max(1 - opacity, floor),深色下超过 58% 之后整段都停在 0.42,滑杆最后
    近一半行程是空的;现在把剩余行程线性铺到保底值上,两端数值不变
    (BackgroundMath.veilOpacity)。
  • 概览解码速度闪烁。 根因不在引擎上报,在我们自己的打点:每个 SSE
    分片喂一个脉冲进连续时间低通,逐次读取。低通的输出随取样时刻连续
    衰减(τ=2.5s 下 0.5s 静默就掉 18%),秒拍与预填充期的 0.4s 快拍各读
    各的,衰减的谷值紧跟着下一个分片的加热峰值——谷—峰交替正是「忽高忽低」
    的本体;分片成批到达(SSE 抖动、MTP/DFlash2 一次验证多个 draft token、
    下游消费慢时 TCP 背压把上游憋成脉冲串)时更严重。新口径是墙钟窗口:
    DecodeWindowMeter.rate(at:) = 窗 [t−W, t] 的量 ÷ 已观测跨度,分子
    分母一起滑,稳态恒等于吞吐,与分片大小、到达分布、轮询相位全部无关;
    边缘压着的分片按「铺在自己的到达时刻上」部分计。窗口右缘钳到最后一个
    事件——秒拍与快拍共用同一个读数也不会错开,早读几十毫秒不会把窗口
    掏空成 0。
  • 收尾后曲线冻结,不再挂 LAST。 GatewayMeter.overview 只把没收尾的
    会话算作 isLive;finish 一落下,当前速度归零、60 秒曲线的右缘冻结在
    那一刻(throughputFrozenAt),不再随挂钟把终点挤出窗口。上一请求的终值
    不再冒充当前速度,也不进曲线;新请求开始后自动解冻。
  • 总解码曲线按 1 秒一拍。 概览不再被 SSE 分片或预填充 chunk 推着
    「立即变化」:引擎轮询统一 1 秒,曲线每拍最多落一个点(≥0.9s 的抖动
    余量),静默段不拿 0 填谷。请求收尾停住整张图;横轴上 1~2 个样本的
    短请求改画点标记,不再被当成空图。
  • 首字时刻不再漏记 role-only 分片。 addTokens 的记账顺序修正:旧
    实现先 guard count > 0 再记 firstTokenAt,空 delta(role-only 首帧、
    finish_reason 帧)整帧忽略——TTFT 被记晚、预填充读数随之偏低。现在
    「流开始回东西」的第一帧就记下(0 token 只记时、不计生成量)。
  • 灵动岛与概览同口径。 胶囊以前引擎 LIVE 计数优先、再落应用内这一路
    的本地测速,与概览 hero 的取数顺序相反,两处数字天然对不上;现在统一
    走 owner 表,只认 LIVE(网关 或 引擎实时),绝不回落 LAST/AVG。

命令行与控制接口

  • 退出改为同步收尾。 applicationShouldTerminate 不再返回 .terminateLater 后靠
    一个 Task 去回信:当前 macOS 会在任务被调度前销毁进程,导致引擎仍驻留、最后
    一次历史写盘丢失。现在在委托里同步走完网关/引擎/下载收尾,最坏等待仍被 1.5 秒
    的引擎宽限期封顶;flashcat app quit 因此能真正等到进程消失并清掉 control.json。
  • 概览保留最后一次请求。 GatewayMeter 不再在结束 6 秒后删除会话:
    「最近请求」保留完成 / 失败行(最多显示 6 条,底层保留 32 条),直到被后续
    请求挤出列表。已结束行的耗时固定在收尾时刻,不会跟着挂钟继续涨;它的全程
    均值只留在行内与累计平均里,当前速度与曲线在收尾时归零 / 冻结,不再挂
    「网关 LAST」。
  • 命令行不再误删版本、也不再误判终端。 engines uninstall omlx@0.7.2 在只装了
    0.7.3 时报错,而不是确认后删掉 0.7.3;chat - 与 --yes 确认改看 stdin 是否
    终端,echo … | flashcat chat - 在终端里也能用;重复 --for 累加而不是只留
    最后一个;表格按 CJK 显示宽度对齐;--control-url 只接受回环地址或 HTTPS,
    不把控制令牌明文发给网络;logs --app --level 与服务端日志过滤口径一致;
    control.json 从创建起就是 0600,不再有"先 0644 再 chmod"的窗口。
  • 控制接口启停竞态。 关闭开关时,正在绑定端口的启动会被作废并关掉新监听,
    不会在设置关闭后留下一个还能驱动应用的接口。
  • flashcat engines start 报「未能就绪:等待超时」,引擎其实已经起来。
    startService() 把启动丢进一个 Task,控制器要到下一次 actor 跳转才进
    .starting;等待方第一眼看到的还是 .stopped,旧实现直接判失败。现在
    .stopped 在 5 秒宽限期内不算失败,见过 .starting 之后再停才是真的停了。

MTPLX 的近前缀恢复

  • GDN boundary 撑爆条目。 MTPLX 的 session bank 预算有 1 GiB 下限,而一条
    19K token 的条目光 GDN boundary 记录就要 700 MB 以上(每条 90 MB、默认最多
    8 条),put() 把这份开销计入条目大小后整条拒绝,bank 里只剩不含
    boundary 的条目;recurrent_boundary_at_or_below() 对这类条目返回 None,
    近前缀恢复每次都被判 boundary_not_better 淘汰,最终报成 ssd_prefix_miss
    ——把 admission 的真因伪装成「SSD 没命中」。启动时下发
    MTPLX_SESSION_BANK_SHED_BOUNDARIES=1,让条目「能留几条 boundary 就留几条」
    地进来,而不是整条被丢;boundary 本身是可重建的加速件,代价只是少几个恢复
    检查点。已用 /health 的 shed_gdn_boundaries_to_fit 字段确认策略生效。

性能

  • 「最近请求」的行改为按请求 id 稳定身份(以前按枚举下标,新请求进表后
    整列行重排、每秒重绘出跳动)。

测试

  • 新增 FlashCatCLICoreTests:参数解析、别名、SSE 解码、控制路由、运行时文件
    权限(含旧文件收紧到 0600)、控制地址校验、版本选择、CJK 表格宽度、配置/医生
    文档编码、输出格式。
  • LocalizationTests 用源码扫描守住「应用内不得再出现快捷键/按键监听」,并覆盖
    新增的 CLI 设置文案。
  • 新增 CacheHitTests:缓存命中率的 owner 顺序(网关样本优先、0% 与「没有样本」
    分开)与按 token 加权的算法;新增 EnginePreferenceTests:写默认模型只记偏好,
    不动 activeID。

工程

本版全部单元测试通过(swift test,含新增的 CacheHitTests、EnginePreferenceTests
与 FlashCatCLICoreTests)。