<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>I am LAZY bones?</title>
	<atom:link href="https://luy.li/feed/" rel="self" type="application/rss+xml" />
	<link>https://luy.li</link>
	<description>AN ancient AND boring SITE</description>
	<lastBuildDate>Sun, 18 Oct 2026 14:52:41 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>Mac 遥控 iPhone</title>
		<link>https://luy.li/2026/10/18/mac-rc-iphone/</link>
					<comments>https://luy.li/2026/10/18/mac-rc-iphone/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sun, 18 Oct 2026 14:52:17 +0000</pubDate>
				<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2620</guid>

					<description><![CDATA[<p>最近在折腾一个 iPhone App 的自动化：Mac 上跑个程序，盯着手机屏幕看，该点哪就点哪。一开始图省事 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/10/18/mac-rc-iphone/">Mac 遥控 iPhone</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>最近在折腾一个 iPhone App 的自动化：Mac 上跑个程序，盯着手机屏幕看，该点哪就点哪。一开始图省事，用的是系统自带的 iPhone Mirroring（iPhone 镜像），截窗口的图来看，往窗口上发鼠标事件来点，跑起来倒也没毛病。</p>
<p>问题是，它跑着的时候，我的电脑就基本废了：鼠标时不时被拽过去点一下，键盘焦点也跟着跑到镜像窗口里，我在终端（用的是我自己写的 <a href="https://github.com/bones7456/notchy">Notchy</a>）里敲的字，搞不好就敲进手机里去了；更别说镜像窗口一旦被别的窗口挡住，截到的就是我自己的浏览器。。。程序一跑就是一两个钟头，这段时间电脑和手机都不能碰，实在受不了。<b>我意识到，不妙，来活儿了～</b><br />
<span id="more-2620"></span><br />
<img fetchpriority="high" decoding="async" src="https://luy.li/wp-content/uploads/2026/10/iphone-remote-control_header.webp" alt="" width="1500" height="844" class="alignnone size-full wp-image-2622" /></p>
<p>最后的结果是：完全不用 iPhone Mirroring，也不用 Xcode 的 DeviceHub，Mac 直接通过 Apple 给开发者用的 CoreDevice 通道，<b>后台截手机的屏、往手机上发触摸</b>，鼠标、键盘、窗口一概不碰。中间踩了几个挺隐蔽的坑，值得写一篇，分享给有需要的朋友。</p>
<h2>先说结论：镜像窗口这条路为什么走不通？</h2>
<p>最先想到的当然是&#8221;别动鼠标，直接把点击事件投给那个进程&#8221;。macOS 有现成的 API：<code>CGEvent.postToPid()</code>，可以把鼠标事件直接发给指定 pid，不经过全局的事件流。</p>
<p>我写了个小工具，三种姿势都试了一遍：只设坐标、再把窗口号写进事件的 <code>mouseEventWindowUnderMousePointer</code> 字段、再先补一个 mouseMoved。结果是——iPhone Mirroring 一概不理，画面纹丝不动：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">pid 75125 窗口 8451 点 (94.0, -682.0) mode b  当前前台 Notchy
鼠标 (668.18, 529.85) -&gt; (668.18, 529.85)  前台 Notchy -&gt; Notchy</pre><p></p>
<p>鼠标确实没动、前台也没换，但 App 根本没收到。用真实的鼠标事件点也不行：窗口不在前台时，第一下点击只是把窗口激活，并不会传给手机（这个之前就踩过，点击&#8221;成功&#8221;了画面却不动，还以为是坐标错了）。</p>
<p>Xcode 27 里 Simulator 改名成了 DeviceHub，它也能投屏真机，我抱着希望又试了一遍，结果一模一样。最后能用的只剩一个土办法：<b>先激活窗口 → 点 → 0.1 秒内把鼠标挪回原位、把原来的前台程序切回来</b>。实测一次 0.3 秒左右，能用，但每点一下焦点都要闪一下，治标不治本。</p>
<h2>真正的入口：CoreDevice 的远程 HID</h2>
<p>既然 DeviceHub 能把我在窗口里的点击传给手机，那它底层一定有个&#8221;往手机发触摸&#8221;的通道。翻了一下它链接的私有框架，在 CoreDevice 里找到了这么几个东西：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">com.apple.coredevice.feature.remote.hid.digitizer
com.apple.coredevice.feature.remote.hid.button
com.apple.coredevice.feature.remote.universalhidservice
com.apple.dt.remote.hid</pre><p></p>
<p>digitizer 就是触摸屏。再用 <code>devicectl</code> 看一眼手机支持的能力，果然在列：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$ xcrun devicectl device info details --device &lt;你的设备名&gt; | grep HID
    &bull; HID Button (com.apple.coredevice.feature.remote.hid.button)
    &bull; HID Digitizer (com.apple.coredevice.feature.remote.hid.digitizer)
    &bull; HID Keyboard (com.apple.coredevice.feature.remote.hid.keyboard)
    &bull; HID Scroll (com.apple.coredevice.feature.remote.hid.scroll)</pre><p></p>
<p><code>com.apple.dt.remote.hid</code> 这个名字说明它是开发者磁盘镜像（DDI）里带的服务（手机上对应的进程叫 dtuhidd），也就是说，手机得开着开发者模式、挂着 DDI 才有。</p>
<p>接下来是怎么调用它。CoreDevice 是私有的 Swift 框架，没有 .swiftinterface，硬调很痛苦。好在开源社区已经有人把协议抓出来了：<a href="https://github.com/doronz88/pymobiledevice3">pymobiledevice3</a> 新版本里有一组 <code>universal-hid-service</code> 命令，<a href="https://github.com/danielpaulus/go-ios/pull/849">go-ios 也有对应的实现</a>，并且在 iOS 27 上验证过。</p>
<p>安装注意一下 Python 版本，我机器上默认的 3.14 编译某个依赖会挂，用 uv 建个 3.13 的环境就好了：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">uv venv -p 3.13 ~/.venvs/pmd3
VIRTUAL_ENV=~/.venvs/pmd3 uv pip install pymobiledevice3</pre><p></p>
<h2>触摸：必须先开一路&#8221;投屏&#8221;</h2>
<p>命令行版本很简单，坐标是把屏幕归一化到 0～65535：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">pymobiledevice3 developer core-device universal-hid-service tap -- 30146 30015</pre><p></p>
<p>我第一次跑，命令正常退出、没有任何报错，手机上——什么都没发生。<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>这是整件事里最阴的一个坑：<b>手机端要求必须有一路媒体流（也就是 Xcode 投屏用的那路视频流）在跑，触摸才会被认</b>。没有媒体流的时候，主机注册的触摸面会被当成&#8221;外接配件&#8221;，backboardd 直接把事件扔掉，而发送那一侧照样返回成功，完全没有提示。pymobiledevice3 的 <code>touch_session</code> 会帮你先开流再发触摸，但它开流以后只等 0.5 秒就开始发，在我的 iPhone 16 Pro 上不够，第一下照样被吞。</p>
<p>所以我干脆自己写，开流以后多等一会儿：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">import asyncio
from pymobiledevice3.remote.userspace_tunnel import UserspaceRsdTunnel
from pymobiledevice3.remote.core_device import hid_service as H

async def main():
    tun = UserspaceRsdTunnel(serial=&quot;&lt;UDID&gt;&quot;)
    rsd = await tun.aopen()
    async with H.touch_session(rsd) as hid:     # 先开媒体流, 拿到认证
        await asyncio.sleep(1.5)                 # 0.5 秒不够, 1.5 秒稳
        x, y = 30146, 30015                      # 0~65535 归一化坐标
        await hid.send_touchscreen(H.TOUCHSCREEN_STATE_CONTACT, x, y)
        await asyncio.sleep(0.07)
        await hid.send_touchscreen(H.TOUCHSCREEN_STATE_RELEASE, x, y)
    await tun.aclose()

asyncio.run(main())</pre><p></p>
<p>这次手机乖乖地点进去了，鼠标和前台程序全程没动。开流大概要 1.1 秒（只在建连接时付一次），之后每次点击只要 80ms 左右。拖动也一样，就是连续发 CONTACT 报告、最后发一个 RELEASE，中间没有&#8221;按下/抬起&#8221;的区分，每个 CONTACT 的意思都是&#8221;现在手指在这儿&#8221;。</p>
<p>另外，媒体流是&#8221;整个会话一直开着&#8221;的，所以正确的姿势是做成一个<b>常驻进程</b>：连接、开流、认证都只做一次，然后等着接命令。</p>
<h2>截屏：210ms 一张，但第 15 张会卡死</h2>
<p>看屏幕这边，<code>devicectl</code> 本身就能截：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">xcrun devicectl device capture screenshot --device &lt;你的设备名&gt; --destination shot.png</pre><p></p>
<p>但每次都要重新建连接，一张 0.6 秒，太慢。pymobiledevice3 里对应的是 <code>ScreenCaptureService</code>，保持一条连接连续截，就快多了：原生分辨率（16 Pro 是 1206×2622）的无损 PNG，一张 200ms 出头。</p>
<p>然后我就被坑了：程序跑了几分钟，发现画面一直停在同一帧，一动不动。。。查下来，不是返回了旧图，而是某一次截图请求发出去以后就再也没回来，后面的请求全被它堵着。</p>
<p>为了确认是谁的锅，我做了个对照：同一条截图连接连续截，开着触摸用的媒体流和不开，各跑一次：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">无触控流: 14 张, 卡住于 [6.7] 秒
有触控流: 14 张, 卡住于 [6.3] 秒</pre><p></p>
<p>两边都是<b>正好第 15 张卡死</b>。一张 PNG 平均 1.19MB，14 张差不多 16MB——这个数太眼熟了，很像是接收窗口满了以后没人去补。（底层具体是哪一层的流控我没有深挖，反正现象非常稳定。）</p>
<p>既然是&#8221;一条连接用到 16MB 就废&#8221;，那就别让它用到 16MB：<b>每截 8 张就换一条新连接</b>。新开一条只要 9ms 左右，几乎没有成本：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">async def capture(self):
    if self.cap is None or self.cap_n &gt;= 8:
        if self.cap is not None:
            await self.cap.__aexit__(None, None, None)
        self.cap = ScreenCaptureService(self.rsd)
        await self.cap.__aenter__()
        self.cap_n = 0
    self.cap_n += 1
    return await asyncio.wait_for(self.cap.capture_screenshot(), timeout=3)</pre><p></p>
<p>改完以后连跑一分钟：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">60 秒 240 张无卡死, 平均 250ms/张, 开连接平均 9ms, PNG 平均 1.19MB</pre><p></p>
<p>注意 <code>wait_for</code> 那个超时也别省，万一哪次真卡了，至少能重连恢复，而不是永远挂着。</p>
<h2>有线还是 Wi-Fi？</h2>
<p>iOS 17 以后，这些服务都要先建一条隧道才能访问。pymobiledevice3 有好几种建法，我实测下来主要是这两种：</p>
<p>1. <b>userspace</b>：在进程里用纯 Python 的网络栈自己建一条隧道，不需要 root。缺点是它走 usbmux，<b>只认数据线连着的设备</b>，Wi-Fi 下会直接报 <code>usbmux has no device matching udid</code>。<br />
2. <b>native</b>：借用 macOS 自己（remoted）已经建好的那条隧道，也不需要 root，Wi-Fi 下也能用。但官方文档里写得很清楚：设备在这条隧道上只保留一个 RSD 连接，它和 remoted 会互相把对方挤掉，开着 Xcode 的时候要小心。</p>
<p>那 Wi-Fi 能不能用呢？我把数据线拔了，换 native 跑了三分钟截图：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">共 29 张, 失败 0, 平均 1133ms, 最慢 1483ms</pre><p></p>
<p>连是能连上，但一张截图要 1 秒多，中间还隔三差五超时重连，算下来 6 秒才拿到一张新画面，完全不能用。（触摸倒是没问题，一个触摸报告才几十字节。）瓶颈就在每张 1.2MB 的无损 PNG，Wi-Fi 扛不住。所以结论是：<b>要实时看屏幕，老老实实插线</b>。真要走无线的话，得改成去解那路 HEVC 视频流，那就是另一个故事了。</p>
<h2>和老代码对接：一个常驻进程 + 一个坐标换算</h2>
<p>我原来的程序是照着 iPhone Mirroring 窗口写的：截图是 652×1440，所有坐标、所有像素判断都基于这个尺寸。换成原生截图以后，最省事的办法不是改程序，而是<b>在常驻进程里把原生截图换算回原来的坐标系</b>，程序那边一行坐标都不用动。</p>
<p>镜像窗口里的手机画面，就是原生画面缩放加平移：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">镜像像素 = s &times; 原生像素 + (ox, oy)</pre><p></p>
<p>参数我是这么求的：同一个界面，镜像截一张、原生截一张，在静止的区域里网格搜索 s、ox、oy，让两张图逐像素的差最小。搜出来非常规整：s = 620/1206（手机画面在镜像窗口里正好 620 像素宽），ox = 16，oy = 76（左右各留 16 像素，上面是镜像窗口自己的标题条）。点击的时候反过来算，再归一化到 0～65535 就行了。</p>
<p>常驻进程我用 asyncio 写，开一个 unix socket，一行一条命令，回一行结果：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">ping                              -&gt; ok
shot &lt;path&gt;                       -&gt; ok &lt;耗时ms&gt;    截图并换算成旧坐标系, 写到 path
tap &lt;x&gt; &lt;y&gt;                       -&gt; ok
drag &lt;x1&gt; &lt;y1&gt; &lt;x2&gt; &lt;y2&gt; [steps]  -&gt; ok</pre><p></p>
<p>协议简单到 shell 里都能直接用，调试的时候随手就能点一下、截一张，挺方便的。</p>
<h2>无损截图的福利：识别可以做到&#8221;一个像素都不差&#8221;</h2>
<p>这是我没想到的收获。以前用镜像窗口的时候，画面是有损的视频流，同一个数字每次截出来都有点不一样，所以识别界面上的数字和图标只能用比较粗的特征，再堆大量样本去覆盖抖动，一直不太稳，偶尔会漏认。</p>
<p>换成原生截图以后，<b>同一个数字、同一个图标，在同一个位置，每次渲染出来的像素是完全一样的</b>。我把录下来的几百帧里，界面上固定位置的数字和图标都裁出来、二值化，然后按&#8221;完全相同&#8221;归类，结果一种字形在一个位置就只有一类，同一类里动辄几十上百帧一模一样。这样建库就很简单了：每类看一眼、标一次。</p>
<p>识别的时候，取笔画的外接框、左上角对齐、允许错开一个像素，然后数有多少像素不一样。我统计了一下&#8221;同一个字形之间&#8221;和&#8221;不同字形之间&#8221;的最小距离：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">同一字形最大差异   不同字形最小差异
大号数字              30                 348
小号数字              45                  95   (最像的一对是 3 和 8)
大号图标              12                 112
小号图标              15                  57</pre><p></p>
<p>两边隔得很开，门槛放中间就行。拿全部录像回放验证，两千多次识别，一次都没认错，认不出来的只有动画过渡中的几帧。（顺带一提，二值化的阈值很关键：一开始我把&#8221;灰度小于 200&#8243;都算作笔画，结果旁边元素投下来的一圈浅灰阴影也算进去了，两个很像的图标就分不开了；改成黑字必须真黑、红字必须真红，立刻就干净了。）</p>
<h2>几个坑</h2>
<p>1. <b>触摸被静默丢弃</b>：没有媒体流、或者开流以后等得不够久，触摸报告照样&#8221;发送成功&#8221;，但手机上什么都不发生。16 Pro 上我等 1.5 秒才稳。<br />
2. <b>截图连接 16MB 就卡死</b>：同一条 ScreenCaptureService 连接截到第 15 张左右，请求就一去不回。定期换连接，外加超时重连。<br />
3. <b>userspace 隧道只认数据线</b>：Wi-Fi 下只能用 native，而 native 会和 Xcode / devicectl 抢连接。<br />
4. <b>收尾会把别人的投屏也关了</b>：pymobiledevice3 的 touch_session 退出时会 <code>stop_media_stream(stop_all=True)</code>，我测试的时候 DeviceHub 里的投屏画面就跟着停了，吓我一跳。反正用了这套就不需要 DeviceHub，把它关掉就好。<br />
5. <b>手机自己会锁屏</b>：这套方案要求手机解锁、App 在前台，记得把&#8221;自动锁定&#8221;设成&#8221;永不&#8221;，不然屏幕一黑就全白干。</p>
<h2>回头看</h2>
<p>现在程序跑起来的时候，电脑终于是我自己的了，想开什么窗口开什么窗口，鼠标再也不会被拽走。唯一的遗憾是手机还是不能碰，毕竟它在给程序干活呢，哈哈。</p>
<p>下一步想试试能不能把那路 HEVC 视频流解出来，这样拔了线、走 Wi-Fi 也能实时看屏幕，就是不知道有损画面会不会把我刚建好的字形库又打回原形。。。到时候再说吧～</p><p>The post <a href="https://luy.li/2026/10/18/mac-rc-iphone/">Mac 遥控 iPhone</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/10/18/mac-rc-iphone/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>老V100跑图片embedding的那些事</title>
		<link>https://luy.li/2026/10/05/v100_embedding/</link>
					<comments>https://luy.li/2026/10/05/v100_embedding/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Mon, 05 Oct 2026 10:37:59 +0000</pubDate>
				<category><![CDATA[备忘]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2615</guid>

					<description><![CDATA[<p>去年我写过一篇用ktransformers跑671b的DeepSeek R1，里面那台&#8221;4块V10 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/10/05/v100_embedding/">老V100跑图片embedding的那些事</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>去年我写过一篇<a href="https://luy.li/2025/02/27/ktransformers_deepseek/">用ktransformers跑671b的DeepSeek R1</a>，里面那台&#8221;4块V100的GPU卡&#8221;的机器，最后只跑出了 1 token/s 左右，基本只能拿来研究研究。没想到一年多过去，它又被我翻出来干活了：这次是给一大批图片做 embedding，用来做以图搜图。用的模型是 <a href="https://huggingface.co/Qwen/Qwen3-VL-Embedding-8B">Qwen3-VL-Embedding-8B</a>，全量跑一遍要十几天。</p>
<p>活儿本身倒是跑起来了，但过程中我被问（也自己问自己）了好几个问题：V100 为啥用不了 BF16？FA2 到底是个啥？每张图 256 个 visual tokens 到底算多还是算少，会不会影响搜索效果？换个 token 数速度又差多少？查资料加实测折腾了一圈，算是把这几个概念理顺了，整理成一篇学习笔记，分享给有需要的朋友。<br />
<span id="more-2615"></span></p>
<h2>先说结论：V100 能用的只有 FP16</h2>
<p>这台机器的配置跟那篇里一样：4 块 V100，每块 16G 显存，计算能力（compute capability）是 7.0，也就是 Volta 架构。这个 7.0 是关键，后面一堆&#8221;用不了&#8221;都是因为它。</p>
<p>8B 的模型，FP16 权重差不多就把一张 16G 的卡塞满了（实测只剩一百来 MB 余量），所以最后的部署方式是每张卡跑一个副本、batch 1，四张卡加起来大概 30 张图/秒。至于为什么不把一个模型切到多张卡上、为什么不开更大的 batch，那又是另一个故事了。。。</p>
<h2>FP16、BF16、FP8 有什么区别？</h2>
<p>浮点数由三部分组成：符号位、指数位、尾数位。<b>指数位决定能表示的范围，尾数位决定精度</b>。理解了这一句，这几种格式的区别就很好懂了：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">格式        位宽   指数/尾数   最大值          大约有效位数   最早支持的硬件
FP32        32     8 / 23     ~3.4e38         ~7 位          所有 GPU
FP16        16     5 / 10     65504           ~3 位          Volta（V100）起有 Tensor Core
BF16        16     8 / 7      ~3.4e38         ~2 位          Ampere（A100）起
FP8 E4M3    8      4 / 3      448             ~1 位          Hopper（H100）/ Ada 起
FP8 E5M2    8      5 / 2      57344           更低           同上</pre><p></p>
<h3>FP16</h3>
<p>FP16 的优点是：在 16 位格式里精度最高，而且 V100 就已经有 Tensor Core 支持，算力是 FP32 的好几倍，显存和带宽占用也减半。</p>
<p>缺点是<b>范围太小</b>，超过 65504 就溢出成 inf，然后就是一路 NaN。大模型的激活值里经常有少量特别大的离群值，尤其是那些用 BF16 训练出来的模型，训练的时候根本不用操心溢出，拿到 FP16 下推理就可能在某一层炸掉。Qwen3-VL 官方就是按 BF16 发布的，在 V100 上只能用 FP16 硬跑（好在我们这边输出一直是正常的）。</p>
<h3>BF16</h3>
<p>BF16 的思路很直接：<b>拿精度换范围</b>。它的指数位和 FP32 一样是 8 位，所以范围也和 FP32 一样大，基本不会溢出，训练时也不需要 loss scaling，从 FP32 转过来几乎没坑。代价是尾数只剩 7 位，精度比 FP16 差。</p>
<p>对深度学习来说，范围比精度重要得多，所以 BF16 成了现在大模型训练的事实标准。可惜要 A100（计算能力 8.0）及以上才支持，V100 就只能看着了。。。</p>
<h3>FP8</h3>
<p>FP8 是再砍一半：Tensor Core 算力大约是 BF16 的 2 倍，权重显存也减半，对推理吞吐很有吸引力。但它的精度和范围都非常有限，<b>必须配合缩放因子</b>（per-tensor、per-channel 或 per-block 的 scale）才能用，还要校准，可能带来能感知到的精度损失。</p>
<p>它有两个变体：E4M3 精度稍高，一般用于前向的权重和激活；E5M2 范围更大，一般用于反向的梯度。需要 H100、L40 这一代的卡。PS：即使是 E4M3，能表示的最小正数，也只是 0.00195 而已，是个什么精度，可以以此为参考。</p>
<p>对 embedding 这种要比较向量相似度的任务，我会比较谨慎：真要上 FP8，得先拿一批固定的图，对比 FP8 和 BF16 算出来的向量 cosine 相似度，再看 top-k 召回的重合率，不能光看速度。</p>
<h2>FA2 又是什么？</h2>
<p><b>FA2（FlashAttention-2）不是数值格式</b>，而是 attention 的一种计算实现（kernel）。它之所以总跟 BF16、FP8 被放在一起说，是因为它们都卡硬件，而 V100 正好都用不上（哈哈，难兄难弟）。</p>
<p>标准的 attention 要先算出一个 N×N 的分数矩阵（N 是序列长度），写回显存，再读回来做 softmax，再乘 V。这个过程的瓶颈其实不在计算，而在<b>显存读写</b>，而且显存占用是 O(N²)，序列一长就爆。</p>
<p><a href="https://github.com/Dao-AILab/flash-attention">FlashAttention</a> 的做法是把 Q、K、V 切成小块，放进 GPU 片上的 SRAM 里，把&#8221;matmul → softmax → matmul&#8221;融合进一个 kernel 里算完。softmax 用&#8221;online softmax&#8221;的技巧逐块更新，<b>完全不需要生成完整的 N×N 矩阵</b>。所以：</p>
<p>1. 结果是精确的，不是近似 attention。<br />
2. 显存从 O(N²) 降到 O(N)。<br />
3. 一般比标准实现快 2～4 倍，序列越长越明显。</p>
<p>但它要求 Ampere 及以上（计算能力 8.0+），只支持 FP16/BF16 输入。去年那篇里我还专门折腾过 flash-attn 的安装，现在回头看，V100 本来就不是它的目标硬件。。。在 V100 上能用的替代是 PyTorch 的 SDPA，它有一个 memory-efficient 后端，能省一些显存，但速度比不上 FA2。</p>
<p>另外要说一句：我们每张图只有 256 个 visual tokens，序列很短，attention 本来就不是主要开销。所以就算换了新卡，<b>提升主要来自更强的 Tensor Core 和更大的显存</b>（终于能开真正的 batch 了），FA2 只是锦上添花。</p>
<h2>256 个 visual tokens 是什么水平？</h2>
<p>先搞清楚一个 token 对应多少像素。Qwen3-VL 的视觉编码器把图切成 16×16 的 patch，再把 2×2 个 patch 合并成 1 个 token，所以 <b>1 个 token = 32×32 像素</b>。注意这跟 Qwen2-VL 的 28×28 不一样，网上很多资料还是按老的算法讲的。</p>
<p>这样算下来，256 tokens 大约是 26 万像素，正方形图就相当于 <b>512×512</b>。非正方形的图会保持长宽比，总像素不变。跟常见的检索模型比一下：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">方案                          等效输入分辨率
CLIP ViT-L/14 @224            224&times;224（256 个 patch）
SigLIP @384                   384&times;384（729 个 patch）
Qwen3-VL @256 tokens          约 512&times;512
Qwen3-VL 默认上限 1280 tokens  约 1145&times;1145</pre><p></p>
<p>所以 256 在 Qwen3-VL 自己的可选范围里算偏低档，差不多只有默认上限的 1/5；但跟业界常用的检索模型比，分辨率一点也不低，像素量差不多是 CLIP 224 的 5 倍。</p>
<h2>它会影响以图搜图吗？</h2>
<p>先理解一点：不管输入多少 token，最后都会被压成<b>一个 4096 维的向量</b>。这个向量主要编码的是图片的整体语义：是什么东西、什么风格、什么颜色、什么构图。token 数决定的是模型&#8221;看得多清楚&#8221;，而一个向量本身能装下的细节也是有上限的。</p>
<p><b>基本不受影响的场景：</b></p>
<p>1. 同一张图，或者压缩、缩放、轻微裁剪、加水印后的近似重复图。实测拿库里已有的图去搜，自己匹配自己的得分是 1.0001，非常稳。<br />
2. 同款商品换了角度、背景或模特，找同类、找风格相似的图。这些靠的是整体外观，512 像素已经足够看清。</p>
<p><b>可能明显受影响的场景：</b></p>
<p>1. 图里的文字：品牌名、型号、价格标签、包装上的小字。512 像素下小字大概率已经糊了，Qwen3-VL 本来不错的 OCR 能力在这里就浪费了。<br />
2. 细粒度区分：同一系列的不同型号、纹理图案上的细微差别、很小的 logo。<br />
3. 小目标：要找的东西只占原图很小一块。<br />
4. 极端长宽比：长截图、长图、横幅。总像素是固定的，长边一拉长，短边就被压得很小。</p>
<p>要说明的是，上面这些是根据原理做的判断，不同 token 数下的召回率我还没有实测过（GPU 正忙着跑全量，抽不出来<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" />）。打算等全量跑完，抽一部分图片分别用 256、512、1280 建几个小索引，按场景分组比一下 recall@10，用数据说话。</p>
<h2>换个 token 数，速度差多少？</h2>
<p>这个倒是实测过的。同一批图，8B 模型，batch 8，计时前已经做完 resize，只算 GPU 部分：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">visual tokens   等效分辨率    每张耗时     相对 256
64              ~256&sup2;        48.7 ms      0.40&times;
128             ~362&sup2;        67.2 ms      0.55&times;
256             ~512&sup2;        121.3 ms     1&times;
512             ~724&sup2;        223.3 ms     1.84&times;
1024            ~1024&sup2;       538.9 ms     4.44&times;</pre><p></p>
<p>可以看到，<b>token 数每翻一倍，耗时大约变成原来的 2.0～2.6 倍，比线性增长还快</b>。原因也好理解：MLP 部分的开销跟 token 数成正比，attention 部分则是 O(n²)。</p>
<p>换算到全量任务上：现在 256 tokens 要跑十几天，换成 512 就得一个多月，换成 1024 得两个多月，而且 1024 在单张 16G 的 V100 上大概率直接 OOM（权重已经快把显存占满了，激活值一大就放不下）。这也是当初选 256 的原因。</p>
<p>另外有两个一开始让我有点意外的结论：</p>
<p>1. <b>原图分辨率几乎不影响 GPU 速度。</b> 256 tokens 预算下，原图是 512²、1024²、2048²、4096² 时，GPU 耗时分别是 102.3、102.9、102.6、102.8 ms，几乎是一条直线，因为图片会先被缩到 token 预算以内。原图越大，只会多花一点 CPU 解码 JPEG 的时间（大约每百万像素 8.4 ms），远远赶不上 GPU 的速度，不会成为瓶颈。所以<b>速度只看 token 预算</b>。<br />
2. <b>长宽比也几乎不影响速度。</b> 图片是等比缩放的，不加 padding 也不裁剪，耗时只跟最终的 token 数有关，3:1 和 1:3 只差 3%。</p>
<h2>几个坑</h2>
<p>1. <b>查询和建库必须用同一个 token 数。</b> 这是最隐蔽的一个坑：库是用 256 建的，而服务端默认是 512。我拿库里已有的一张图去搜，用 512 算出来的向量跟库里自己的相似度只有 0.938，换成 256 才是 1.0001。它不报任何错，只是悄悄返回更差的结果。这也说明 token 数一改，向量就变了，以后想升级只能全量重算，新旧向量绝对不能混用。<br />
2. <b>测速时别把 resize 算进去。</b> 直接计时 <code>encode()</code> 的话，里面包含了 processor 的 resize，而 resize 的耗时随原图像素增长，会让原本不变的 GPU 耗时看起来从 512² 到 4096² 翻了三倍。我差点就得出&#8221;原图越大越慢&#8221;的错误结论。<br />
3. <b>直接调 <code>processor.image_processor(images=...)</code> 会绕过 token 预算。</b> 预算是作为 <code>min_pixels</code>/<code>max_pixels</code> 在 embedder 的处理流程里生效的，单独调 image_processor 得到的 token 数跟实际流水线对不上，拿来做估算是没有意义的。<br />
4. <b>长宽比超过 200 会直接报 <code>ValueError</code>。</b> 不会优雅降级。如果图片来源不可控（比如用户上传），得在上游先校验一下。</p>
<h2>最后</h2>
<p>折腾了一圈，结论其实挺朴素的：V100 只能用 FP16，没有 BF16、FA2、FP8；256 tokens 做以图搜图基本够用，但小字和细节是短板；想要更高的 token 数，代价几乎是翻倍往上涨的。</p>
<p>这台老伙计去年跑 DeepSeek 只有 1 token/s，今年总算干上了一份正经工作，接下来还要连轴转二十来天。vibe了一阵之后，利用率已经压榨地比较狠了，如图：<br />
<img decoding="async" src="https://luy.li/wp-content/uploads/2026/10/V100x4.webp" alt="" width="1500" height="364" class="alignnone size-full wp-image-2617" /></p>
<p>看看能不能顺利跑完吧～</p><p>The post <a href="https://luy.li/2026/10/05/v100_embedding/">老V100跑图片embedding的那些事</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/10/05/v100_embedding/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>博客安全头踩坑记</title>
		<link>https://luy.li/2026/09/26/https_headers/</link>
					<comments>https://luy.li/2026/09/26/https_headers/#comments</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sat, 26 Sep 2026 00:18:14 +0000</pubDate>
				<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2606</guid>

					<description><![CDATA[<p>我这个博客一直是自部署的，很久之前就装了 Really Simple Security（那时候还叫Really [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/09/26/https_headers/">博客安全头踩坑记</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>我这个博客一直是自部署的，很久之前就装了 <a href="https://wordpress.org/plugins/really-simple-ssl/">Really Simple Security</a>（那时候还叫Really Simple SSL） 这个插件，最近它一直提示我的博客不安全，就按它提供的链接<a href="https://scan.really-simple-ssl.com/?domain=https://luy.li">扫了一下</a>，结果页面上一排 &#8220;Not Found&#8221;，HSTS、CSP、Referrer-Policy……六个安全头一个都没有。我心想，不对啊，好歹也是跑了这么多年的站，怎么会一个都没有？（后来看扫描页才发现，帮你加这些头是它 Pro 版的功能，免费版只负责提醒你。。。）</p>
<p><span id="more-2606"></span><br />
<img decoding="async" src="https://luy.li/wp-content/uploads/2026/09/blog_headers.webp" alt="" width="1500" height="711" class="alignnone size-full wp-image-2609" /></p>
<p>curl 一下看看：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$ curl -sI https://luy.li/
HTTP/2 200
server: nginx/1.24.0 (Ubuntu)
date: Fri, 25 Sep 2026 06:54:12 GMT
content-type: text/html; charset=UTF-8
link: &lt;https://luy.li/wp-json/&gt;; rel=&quot;https://api.w.org/&quot;</pre><p></p>
<p>还真是干干净净，除了 nginx 很热情地把自己的版本号和系统都报出来了，啥安全相关的头都没有。。。行吧，补。结果补着补着，把同域名下的另一个服务给补挂了，哈哈。整个过程学到不少东西，写下来，分享给有需要的朋友。</p>
<h2>这六个头都是干嘛的？</h2>
<p>先说结论：这几个都不是&#8221;漏洞&#8221;，而是&#8221;浏览器端的防护没开&#8221;。WordPress 默认不加，nginx 默认也不加，所以只能自己加。</p>
<p>1. Strict-Transport-Security（HSTS）：告诉浏览器&#8221;以后只许用 HTTPS 访问我&#8221;。我的 80 端口虽然早就 301 到 https 了，但用户每次敲 luy.li 的时候，都会先发一次明文请求再被跳走，公共 Wi-Fi 下就有被劫持降级的可能。有了 HSTS，浏览器会直接在本地改成 https，明文请求压根不发出去。不过它保护不了第一次访问（得先收到过这个头才行），想连第一次也保护，就得靠后面会说到的 preload。<br />
2. X-Content-Type-Options: nosniff：不让浏览器瞎猜文件类型，防止有人传个伪装成图片的 HTML 被当脚本执行。零副作用，无脑加。<br />
3. Referrer-Policy：跳去外站的时候少带点信息。推荐 <code>strict-origin-when-cross-origin</code>，跨站只带域名，不带完整路径和参数。<br />
4. Permissions-Policy：把摄像头、麦克风、定位这些用不上的浏览器能力直接关掉。<br />
5. X-XSS-Protection：这个比较有意思，推荐值是 <code>0</code>，也就是<b>关掉</b>。老浏览器内置的 XSS 过滤器本身就有漏洞，已经被废弃了，明确关掉反而更安全。<br />
6. Content-Security-Policy（CSP）：白名单，限制页面能从哪加载脚本、样式、能不能被别人 iframe 嵌套。防 XSS 最有效的一层，但也是最容易把站搞坏的一个。</p>
<h2>nginx 里怎么加？</h2>
<p>在 443 的那个 <code>server {}</code> 里加上：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag"># 隐藏 nginx 版本号
    server_tokens off;

    # 安全响应头（注意：任何 location 里只要自己写了 add_header，就不会再继承这里的所有 add_header）
    add_header Strict-Transport-Security &quot;includeSubDomains; preload; max-age=63072000&quot; always;
    add_header X-Content-Type-Options &quot;nosniff&quot; always;
    add_header Referrer-Policy &quot;strict-origin-when-cross-origin&quot; always;
    add_header Permissions-Policy &quot;camera=(), microphone=(), geolocation=(), payment=()&quot; always;
    add_header X-XSS-Protection &quot;0&quot; always;
    add_header Content-Security-Policy &quot;upgrade-insecure-requests; frame-ancestors 'self'; object-src 'none'; base-uri 'self'&quot; always;</pre><p></p>
<p>几个要注意的地方：</p>
<p>1. <b>add_header 的继承是&#8221;全有或全无&#8221;</b>。某个 location 里只要自己写了一条 add_header（比如给静态文件加 Cache-Control），server 级的所有 add_header 在这个 location 里就全部失效了。这个坑非常隐蔽，加完一定要挨个 location curl 一下。<br />
2. 带上 <code>always</code>，不然 404、500 这些错误页是不带这些头的。<br />
3. CSP 我只给了个宽松版。看了下首页，脚本倒是都来自本站，但 WordPress 和插件会往页面里塞好几段内联 script，要是写 <code>script-src 'self'</code>，页面立马就坏了。所以我只做了不会弄坏页面的几件事：升级混合内容、禁止被别的站 iframe 嵌套（顺便防了点击劫持）、禁 object、锁 base。想收紧的话，先用 <code>Content-Security-Policy-Report-Only</code> 跑几天，看控制台报了啥再说。</p>
<p>改完 <code>nginx -t &amp;&amp; systemctl reload nginx</code>，再 curl：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$ curl -sI https://luy.li/
HTTP/2 200
server: nginx
...
strict-transport-security: includeSubDomains; preload; max-age=63072000
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), payment=()
x-xss-protection: 0
content-security-policy: upgrade-insecure-requests; frame-ancestors 'self'; object-src 'none'; base-uri 'self'</pre><p></p>
<p>server 那行也不再报版本号了，舒服～</p>
<h2>顺手堵上的几个口子</h2>
<p>扫描器只看响应头，但既然都动配置了，WordPress 常见的几个口子也顺便看了看。</p>
<p><b>xmlrpc.php</b>。这个默认是开着的，而且支持 <code>system.multicall</code>，意思是一个请求里可以塞上百组用户名密码去试，爆破效率极高。我又不用手机 App 发文章，直接关：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">location = /xmlrpc.php {
        deny all;
    }</pre><p></p>
<p><b>readme.html 和 license.txt</b>。这俩文件能直接访问，里面写着 WordPress 版本号，等于告诉别人该拿哪个版本的漏洞来试。同样 deny 掉（删文件也行，但每次升级 WordPress 会重新生成，不如 nginx 里一劳永逸）。</p>
<h2>HSTS 是不认端口的！</h2>
<p>到此，我以为都结束了，但看进度条就知道（喂，你是blog哎，哪有进度条？）还没完。配置上线之后，我检查了一下在这个域名下还跑着另一个服务，之前是 <code>http://luy.li:端口号</code> 这种明文HTTP地址访问的，发现已经挂掉了。</p>
<p>HSTS 是按<b>主机名</b>生效的，跟端口没有关系（<a href="https://datatracker.ietf.org/doc/html/rfc6797">RFC 6797</a> 里写得明明白白）。浏览器只要从 <code>https://luy.li</code> 收到过一次 HSTS 头，之后访问 <code>http://luy.li:任意端口</code> 都会被自动改写成 <code>https://luy.li:同一个端口</code>。而那个端口上跑的是 HTTP，并且我不想改成 HTTPS。</p>
<p>也就是说，部署完之后，所有看过我博客的浏览器，再打开那个服务都会连不上，而且这个状态会被浏览器记两年。好在非浏览器的客户端一般不遵守 HSTS，只有浏览器会中招，不幸中的万幸 <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>咱们换个思路：HSTS 只管下发它的那个主机名，如果我不加 <code>includeSubDomains</code>，它就管不到子域名。刚好我的 DNS 配了泛解析，随便起个子域名就能解析到同一台机器上，换成 <code>http://xxx.luy.li:端口号</code> 访问，一行配置都不用改。可问题是，扫描器推荐的值里偏偏就带着 <code>includeSubDomains</code>。。。那能不能加上它，再给那个子域名开个白名单呢？</p>
<h2>includeSubDomains 能加白名单吗？</h2>
<p>不能。HSTS 标准里压根没有排除机制：</p>
<p>1. 端口：HSTS 完全不看端口，没法单独放过某个端口。<br />
2. 子域名：父域名一旦带了 <code>includeSubDomains</code>，所有子域名、不管几层都被覆盖。子域名自己返回 <code>max-age=0</code> 也没用，它只能清掉自己的那条策略，管不了父域名的。<br />
3. preload：header 里写上 <code>preload</code> 只是个表态，要去 <a href="https://hstspreload.org/">hstspreload.org</a> 提交、审核通过，才会真正被写进浏览器的内置列表。它的硬性要求就是必须带 <code>includeSubDomains</code>，所有子域名都得能正常走 HTTPS（我的证书一开始就没包含 www，差点栽在这）。而且一旦进了列表，想撤出来得等浏览器发新版，几个月起步。</p>
<p>所以，如果某个服务就是要跑明文 HTTP，又想上 <code>includeSubDomains</code>，只剩两条路：给它另起一个独立的域名，或者直接用 IP 访问（HSTS 对 IP 不生效）。我似乎有洁癖，所以选择了换个域名跑，扫描就全绿了。</p>
<h2>浏览器把 HSTS 存哪了？</h2>
<p>踩完这个坑，自然会想：已经中招的浏览器，怎么把 luy.li 的 HSTS 记录删掉？</p>
<p>Chrome 最方便。打开 <code>chrome://net-internals/#hsts</code>（Edge 是 <code>edge://net-internals/#hsts</code>），先在 &#8220;Query HSTS/PKP domain&#8221; 里查一下 luy.li，能看到记录就说明中招了；再在 &#8220;Delete domain security policies&#8221; 里输入 luy.li，点 Delete 就行。底层文件在 Profile 目录下的 <code>TransportSecurity</code>，是个 JSON，但里面的主机名是哈希过的，直接看是看不出来的。注意这里只能删浏览器动态记下的，preload 内置列表里的删不掉。</p>
<p>Firefox 在历史记录里找到这个站，右键&#8221;忘记此网站&#8221;，不过会把 Cookie 什么的一起清掉。底层文件是 Profile 目录下的 <code>SiteSecurityServiceState.bin</code>。</p>
<p>Safari 的话，我在 <code>~/Library</code> 下搜了一圈，现在的 macOS 是各个 App 自己存一份的，比如 <code>Caches/com.apple.helpd/HSTS.plist</code>、<code>Caches/us.zoom.xos/WebKit/HSTS</code>。Safari 是沙盒应用，它那份应该在 <code>~/Library/Containers/com.apple.Safari/</code> 下面，终端去访问直接报 <code>Operation not permitted</code>，除非给终端开&#8221;完全磁盘访问权限&#8221;。所以老老实实走界面吧：Safari 设置 → 隐私 → 管理网站数据，搜 luy.li，移除，然后彻底退出 Safari 再开。</p>
<p>不过要清楚一点：删了也白删。只要这个浏览器再打开一次 <code>https://luy.li</code>，HSTS 头又会被写回来。删除只适合临时救急，想长期共存，还是得像上面那样把服务挪到别的域名上。</p>
<h2>几个值得记住的坑</h2>
<p>1. nginx 的 add_header：location 里写了任何一条，就不再继承 server 级的全部 add_header。<br />
2. HSTS 按主机名生效，不认端口。同一个域名下只要有一个端口跑明文 HTTP，就不能在这个域名上开 HSTS。<br />
3. includeSubDomains 没有白名单，preload 基本是单行道，加之前想清楚。<br />
4. 撤销 HSTS 不能只删掉这个头，要返回 <code>max-age=0</code>，已经缓存了策略的浏览器才会清掉。已经进了 preload 列表的，这招也不管用。</p>
<p>好了，我去把各个设备上的书签都换成其他域名了。本来只是想让扫描器变绿，结果差点把自己的另一个服务搞瘫。。。<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p><p>The post <a href="https://luy.li/2026/09/26/https_headers/">博客安全头踩坑记</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/09/26/https_headers/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>rtorrent 挪文件后续种那些事</title>
		<link>https://luy.li/2026/09/25/rtorrent-after-mv/</link>
					<comments>https://luy.li/2026/09/25/rtorrent-after-mv/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Fri, 25 Sep 2026 05:02:57 +0000</pubDate>
				<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2601</guid>

					<description><![CDATA[<p>前两天整理 Jellyfin 的媒体库，把电影和剧集分到了不同目录（因为剧集在电影库里会展开显示，就很乱），一 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/09/25/rtorrent-after-mv/">rtorrent 挪文件后续种那些事</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>前两天整理 <a href="https://luy.li/2026/02/20/jellyfin/">Jellyfin</a> 的媒体库，把电影和剧集分到了不同目录（因为剧集在电影库里会展开显示，就很乱），一通 mv 下来，Jellyfin 那边是清爽了。然后我突然想起来：这些片子都是古老但依旧好用的 rtorrent 下的，而 rtorrent 一直开着在做种呢！我意识到，不妙，来活儿了～</p>
<p>好在折腾一圈之后，文件不用挪回去，种子也没重新下，校验 100% 继续做种。中间踩了几个挺反直觉的坑，网上中文资料也不多，值得写一篇，分享给有需要的朋友。<br />
<span id="more-2601"></span></p>
<h2>mv 之后，rtorrent 会怎样？</h2>
<p>先说结论：<b>不会马上出问题，但迟早会出问题。</b></p>
<p>同一个文件系统内的 mv 其实只是改了个名字，文件本身没动。rtorrent 手里如果还攥着这个文件的句柄，就还能照常读，所以刚 mv 完的时候，上传可能看起来一切正常。</p>
<p>但 rtorrent 会定期关掉空闲的文件，下次再按原路径去打开，就找不到了。更要命的是重启 rtorrent 之后，它会重新校验，原路径下啥都没有，种子直接被判成 0%。这时候要是一启动，它就会勤勤恳恳地在原目录把几十个 G 重新下一遍。。。</p>
<p>所以正确的做法是：告诉 rtorrent 文件搬到哪儿了。</p>
<h2>正确的改目录姿势</h2>
<p>rtorrent 里改种子目录有两个命令，区别挺关键：</p>
<p>1. <code>d.directory_base.set</code>：填的是种子的<b>最终路径</b>。多文件种子就是那个种子文件夹本身。<br />
2. <code>d.directory.set</code>：填的是<b>父目录</b>。对多文件种子，rtorrent 会自动在后面拼上种子自己的名字。</p>
<p>我最后是用第二个搞定的（原因见下一节）。完整流程如下，在 rtorrent 界面里选中这个种子：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">Ctrl-D                                   # 停止种子（只按一次！连按两次就会从列表里删除掉）
Ctrl-X  d.close=                         # 关闭种子，状态从 [OPEN] 变成 [CLOSED]
Ctrl-X  d.directory.set=/DOWNLOAD/PATH   # 只填父目录
Ctrl-X  print=$d.base_path=              # 看一眼当前路径对不对
Ctrl-R                                   # 重新校验
Ctrl-S                                   # 启动，继续做种</pre><p></p>
<p>校验跑完显示已下载和总大小一致，就说明路径改对了。最后顺手存一下会话，免得 rtorrent 还没来得及自动保存就被重启，新路径又丢了：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">Ctrl-X  session.save=</pre><p></p>
<h2>踩到的坑</h2>
<h3>1. 种子没关，目录改不了</h3>
<p>我一开始 Ctrl-D 停了种子就去改目录，结果底部蹦出来一行：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">Input failed: Cannot change the directory of an open download atter the files have been moved.</pre><p></p>
<p>（对，atter，rtorrent 源码里就是这么拼的，哈哈，方便大家原样搜索）</p>
<p>意思是：种子还处于 open 状态，而且已经在原目录创建过文件，这时不让改目录。<b>Ctrl-D 只是停止，并不是关闭</b>，得再用 <code>d.close=</code> 把它关掉才行。</p>
<p>还有一个细节：<b>Ctrl-R 校验会把种子重新打开</b>。所以顺序一定是先关、再改、最后校验。我中间手快按了一次 Ctrl-R，列表里又变回了 <code>[OPEN]</code>，还显示 0%，当时着实吓了一跳：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">* [OPEN]   0.0 / 28395.6 MB Rate: 0.0 / 0.0 KB Uploaded: 200046.0 MB
* Inactive: Cannot change the directory of an open download atter the files have been moved.</pre><p></p>
<p>这时千万别 Ctrl-S，一启动就开始重新下载了。</p>
<h3>2. Ctrl-X 输入框吃掉了中文</h3>
<p>这个坑最隐蔽。我的种子目录叫 <code>[太平年].Swords.into.Plowshares.S01.2026.1080p.IQ.WEB-DL.H264.AAC-CMCTV</code>，用 <code>d.directory_base.set</code> 把完整路径粘进去之后，没报错，看着挺顺利。结果 print 一下：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">/home/shared/TV/[].Swords.into.Plowshares.S01.2026.1080p.IQ.WEB-DL.H264.AAC-CMCTV</pre><p></p>
<p>「太平年」三个字凭空消失了！<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 列表里明明能正常显示中文，但 Ctrl-X 的命令输入框很可能只收 ASCII，粘贴进去的中文被悄悄丢掉了，目录就被改成了一个根本不存在的路径。</p>
<p>解决办法就是前面说的 <code>d.directory.set</code>：只填纯英文的父目录 <code>/home/shared/TV</code>，中文部分让 rtorrent 从种子文件里自己取，一个汉字都不用输入。单文件种子也一样，填文件所在的目录就行，文件名也是从种子里取的。</p>
<p>前提是：<b>挪过去的文件夹名和文件名，必须和种子里的一模一样。</b>为了让 Jellyfin 识别得好而顺手改名的，就续不了种了。</p>
<h2>顺手整理：rtorrent 常用按键和命令</h2>
<p>rtorrent 是纯键盘操作，没有菜单可点，平时用熟的就那几个键，一到这种时候就得现翻文档。。。干脆把常用的整理一下，下次直接来这儿抄。</p>
<h3>主界面按键</h3>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">&uarr; / &darr;          在种子列表里上下选择
&rarr;              进入选中种子的详情页（Peer、文件列表、Tracker 等）
&larr;              返回上一级
1 ~ 9          切换视图，1 是 main，其余是 started/stopped/complete/seeding 这类筛选视图

Ctrl-S         启动种子
Ctrl-D         停止种子；对已停止的种子再按一次 = 从列表移除（不删文件）
Ctrl-K         停止并关闭种子（相当于 d.stop= 加 d.close=）
Ctrl-R         重新校验
Ctrl-O         修改种子的保存目录（需要种子处于关闭状态）
+ / -          调整种子优先级

Backspace      输入种子文件路径或 URL，添加并立即开始
Enter          输入种子文件路径或 URL，添加但不开始

a / s / d      全局上传限速 +1 / +5 / +50 KB
z / x / c      全局上传限速 -1 / -5 / -50 KB
A S D / Z X C  同上，改的是下载限速

Ctrl-X         输入命令（下面那一堆都靠它）
Ctrl-Q         退出，再按一次强制退出</pre><p></p>
<p>对，<b>Ctrl-K 其实一步就能完成「停止 + 关闭」</b>，比我前面先 Ctrl-D 再 <code>d.close=</code> 省事。不过拆开写更能看清楚「停止」和「关闭」是两回事，就先这样吧，哈哈。</p>
<p>Ctrl-O 也能改目录，但它同样是在 rtorrent 的输入框里敲路径，碰到中文目录名应该也逃不过被吞的命运，所以我还是更推荐 <code>d.directory.set</code> 只填父目录的办法。</p>
<p>在详情页里选中文件列表（File list）时，按空格可以切换单个文件的优先级，比如 off 掉不想下的文件，只下合集里的某几集。</p>
<h3>Ctrl-X 常用命令</h3>
<p>以 <code>d.</code> 开头的命令作用于当前选中的种子，其他的是全局命令：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">d.start=                           启动
d.stop=                            停止
d.close=                           关闭
d.erase=                           从列表移除（不删文件）
d.check_hash=                      重新校验，同 Ctrl-R
d.tracker_announce=                立即向 Tracker 汇报一次

d.directory.set=/path              改目录，填父目录，种子名自动拼接
d.directory_base.set=/path         改目录，填最终路径
print=$d.base_path=                打印当前种子的实际路径
print=$d.name=                     打印种子名

throttle.global_up.max_rate.set_kb=1000     全局上传限速（KB/s，0 表示不限）
throttle.global_down.max_rate.set_kb=5000   全局下载限速
session.save=                      立即保存会话</pre><p></p>
<p><code>print=</code> 的输出会显示在界面最底部那一行，想确认某个值的时候特别好用。</p>
<h3>~/.rtorrent.rc 里常用的几行</h3>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">directory.default.set = /data/torrents     # 默认下载目录
session.path.set = ~/.session              # 会话目录，存种子状态，重启不丢
schedule2 = watch_directory,5,5,load.start=/data/watch/*.torrent
                                           # 监视目录：往里丢 .torrent 就自动开始下载</pre><p></p>
<p>session 目录一定要配，否则 rtorrent 一重启，所有种子都没了。watch 目录也很方便，配合 Samba 或者 Syncthing，在别的电脑上把种子文件往里一扔就行。</p><p>The post <a href="https://luy.li/2026/09/25/rtorrent-after-mv/">rtorrent 挪文件后续种那些事</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/09/25/rtorrent-after-mv/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>《一句顶一万句》读书笔记</title>
		<link>https://luy.li/2026/09/13/978-7-5354-3976-5/</link>
					<comments>https://luy.li/2026/09/13/978-7-5354-3976-5/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sun, 13 Sep 2026 03:55:47 +0000</pubDate>
				<category><![CDATA[读书笔记]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2597</guid>

					<description><![CDATA[<p>最近，我读的书是《一句顶一万句》。读这本书，不是因为朋友给我介绍说这本书好看，也不是因为这书得了很多奖非常畅销 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/09/13/978-7-5354-3976-5/">《一句顶一万句》读书笔记</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>最近，我读的书是《一句顶一万句》。读这本书，不是因为朋友给我介绍说这本书好看，也不是因为这书得了很多奖非常畅销，而是因为刘震云老师的江湖地位近几年特别高，很难让人不注意到他。所以我就找了一本刘老师的书来读。</p>
<p>这个开头，是不是还挺符合这本书的写作风格的？哈哈。<br />
<span id="more-2597"></span></p>
<p>在这个注意力高度分散的年代，各种短视频平台上，常常会有刘震云这种文人参加综艺的切片。起的标题往往是：刘震云怒怼XXX，点进去就会发现，文化人怼人确实厉害，不动声色地怼得对方哑口无言。而他说话的那种不慌不忙地、慢吞吞地感觉，就和读他的书的感觉是一致的。</p>
<p>这本书里，也大多都是短句，用词也并不深奥偏僻，甚至有点朴实。对话描写经常是说一句话，“又说”。。。“又说”。。。每句也都是短句。</p>
<p>但他就是用这种风格，写出了<strong>两个时代的同一种落寞</strong>。上部我认为大概是发生在民国时期后期或者刚建国时期的故事，下部却大概是世纪交接的年代。虽然时代不同，但主人公确是有一丝丝联系的，而且人物的关系网也都是最底层的劳动人民。可以看出，作者确实也是底层出身，观察非常细致，积累也非常深厚。</p>
<p>总之，在我看来这书挺艺术的。但一句顶一万句，那句话到底是什么话，直到最后也都没有说，突然就结束了，就有点难受。。。不过我也理解这是一种写作手法，哈哈！</p>
<p>剧情就不展开说了，不然算剧透，也不太好。听说有部同名电影，是刘震云担任编剧，还是他女儿执导的，我这就去找来看看。</p><p>The post <a href="https://luy.li/2026/09/13/978-7-5354-3976-5/">《一句顶一万句》读书笔记</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/09/13/978-7-5354-3976-5/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
