AI Pulse

24G显卡跑27B模型:中途降级+KV缓存移植,更接近高精度

自上一篇帖子以来,我一直在思考动态性能降级的不同选项,试图从我的GPU中挤出尽可能多的高质量推理。

周末我读了一篇非常有趣的论文:Cache-to-Cache: Direct Semantic Communication Between Large Language Models。在这篇论文中,作者描述了运行多LLM智能体系统。但他们并不是通过一个harness+工具调用+消息的机制让智能体相互对话,而是通过将一个智能体的kvcache直接融合到另一个智能体的kvcache中来传递上下文。

假设这是可行的,你可以想象这是一种更快速、更完整的在智能体之间传递上下文的方式:与其让一个智能体生成摘要/交接消息,你实际上是直接取出它的工作记忆并移植到目标模型上。

他们接着描述了具体做法,简而言之,他们训练了一个小型神经网络,使其能够在源模型和目标模型的内部表示之间进行“转换”,从而允许融合不同大小甚至不同架构的模型的kvcache。

这让我想到:如果我想在同一模型的不同量化版本之间重用kvcache会怎样? 我的意思是,相同的架构,相同的训练过程……即使没有训练“转换器”,它们不也应该兼容吗?

那么,如果我开始推理时使用高精度的量化版本,然后在设备空间不足时换入较低精度的量化版本让它“接管”,会发生什么?我能比从一开始就运行较低精度的量化版本获得更好的结果吗?

剧透一下,所有这些问题的答案都是是的(在我运行的基准测试中)! 我在下面详细说明了我运行的具体实验。

方法

我生成了不同上下文长度下的困难NIAH风格任务,并让5种不同的Qwen3.8量化策略一决高下!

对于这些任务,我使用了Qwen3.8-27B的三种不同量化版本,每个都由unsloth制作:

- UD-Q6_K
- UD-Q4_K_XL
- UD-IQ3_S

策略

从这些量化版本中,我定义了三种静态量化策略来运行任务:

1. IQ3_S:f16 kvcache,最大ctx 196,096
2. Q4_K_XL:q8_0 kvcache,最大ctx 183,296
3. Q6_K,f16 kvcache,最大ctx 175,104

请注意,Q3和Q4策略的ctx窗口是为24 GiB GPU设计的,而Q6_K情况需要超过24 GiB才能运行。这是为了评估小设备上的静态量化策略与大设备上的静态量化策略相比表现如何。

其想法是看看动态量化策略能否弥补部分差距!

说到这个,我定义了两种动态量化策略,分别与每个小精度静态模型情况进行比较。这些动态量化策略同样都将ctx限制设为适合24 GiB GPU。

IQ3_S 对比

对于这个策略,我使用模型量化最差为IQ3_S的多量化策略来运行任务:

- 以Q6_K开始任务,f16 kvcache,最大ctx 54,272
- 然后换入Q4_K_XL,f16 kvcache,最大ctx 109,312
- 然后换入IQ3_S,f16 kvcache,最大ctx 192,096

在数据中,你可以看到这个策略被标记为Q6→Q4→Q3, f16 KV。

Q4_K_XL, q8_0 kv 对比

对于这个策略,我使用量化最差为Q4_K_XL, q8_0 kv的多量化策略来运行任务:

- 以Q6_K开始任务,f16 kvcache,最大ctx 54,272
- 然后将模型的kvcache量化到q8_0。最大ctx:91,136
- 然后换入Q4_K_XL,f16 kvcache,最大ctx 109,312
- 然后将模型的kvcache量化到q8_0。最大ctx:183,296

(对于运行中段的量化操作,我使用了我的上一篇帖子中描述的热重载方法。f16<->q8_0转换由同一个llama.cpp分支处理。)

在数据中,你可以看到这个策略被标记为Q6/f16→Q6/q8→Q4/f16→Q4/q8。

任务

我生成了数十个独特的NIAH(“大海捞针”)任务,这些任务指示模型解析大量输入文本,并遵循散布在文本中的具体指令来检索一个秘密值。(部分原始素材感谢gkamradt/needle-in-a-haystack)

我选择NIAH是因为它看起来是一种合理的方式来评估多量化策略的连贯性。每个任务要求模型推理出一系列埋藏在干扰文本中的“步骤”,因此移植了kvcache的模型需要能够精确地在源模型中断的地方接上思路。

另外,NIAH不需要复杂的测试设置,答案也是客观的对或错。

对于每个任务和测试用例,我测量了以下内容:

* 结果(正确/不正确)
* 生成的总token数
* 总耗时

尽管每种量化版本的prefill/解码速度非常相似,我还是包含了耗时,因为我想证明多量化方法不会比单量化策略花费明显更长的时间。将kvcache从一个量化版本转移到另一个意味着我们不需要重复prefill!

结果

总的来说,我设计的基准测试任务总计180次独立运行,这些运行让我的GPU耗时14小时26分钟才完成。

其中最大的问题是IQ3_S/f16策略。尤其是在较重的任务上,它总是生成超过5万推理token,并且经常在彻底耗尽上下文窗口(约196k)后仍未能生成响应。

不过,在较短的任务上,它的表现还算合理,甚至在与Q6/f16的一致性方面得分高于Q4_K_XL, q8_0。大概要归功于xhigh推理吧!

说到与Q6/f16的一致性,对我来说这是一个需要追踪的重要指标,因为我想比较动态方法在接近高精度参考模型方面能有多大的改善。

与Q6/f16的一致性

_见图1_

https://preview.redd.it/739ykn0h8qrh1.png?width=1057&format=png&auto=webp&s=959e05ef0110a00d687bd288cb67c254725fb27c

该图显示了最终答案与Q6/f16结果完全相同的任务数量,即使该答案不正确。这里的动机是判断动态量化策略能否接近Q6/f16,我认为这张图强力表明它是可以做到的!

在这两种情况下,动态量化版本的结果与Q6/f16模型的一致性都比对应的静态版本要高得多。总体而言,在长上下文下,绕过IQ3_S的路径最终更接近Q6,这并不太令人意外!

我不想过分强调任务正确性,因此这里重点关注“与Q6/f16的一致性”。这是因为我不确定我的NIAH任务是否能代表总体性能。(话说回来,如果你好奇的话,我确实在下面包含了任务正确性结果。)

平均推理时间与平均输出token

_见图2和图3_

https://preview.redd.it/i7tmdbdm8qrh1.png?width=1057&format=png&auto=webp&s=aa475a88ee31d5f334ccb541be8f3594598572b3

https://preview.redd.it/i4klmw0l8qrh1.png?width=1057&format=png&auto=webp&s=e72d0d3f54e12012d999d5456135a07d6bedd2ba

这些图显示了已完成任务种子的推理时间(秒)和总输出token的算术平均值,包括不正确和上下文耗尽的运行。

我特别想强调推理时间,因为对于动态策略,它包括了换出模型权重和量化kvcache的时间!

我认为这很好地展示了这里的优势——跨任务的推理时间实际上并没有因为我们在使用花哨的动态量化策略而变差。这是因为:

- 当更换模型权重(例如Q6->Q4)时,我们是在直接进行KV缓存移植,直接将kvcache从一个量化版本移动到另一个。
- 当量化现有模型的kvcache(例如Q6/f16->Q6/q8)时,我使用的是我的llama.cpp分支,它能够热重载活动模型的上下文/运行时,并自动在kvcache精度之间转换。

简而言之,在这两种情况下,都不需要重复prefill! 转换后,模型会精确地从它们中断的地方继续prefill/解码。

任务正确性

这里是一个所有策略/运行的任务正确性表格。我按“使用的最低模型精度”对结果进行了划分,以便更容易进行静态-动态比较。

最坏情况 IQ3_S:

策略10k25k50k75k
Q3/f166/109/102/104/10
Q6->Q4>Q37/107/107/104/10

结果:动态量化在2种情况下胜过静态,平局一次,输一次。

最坏情况 Q4_K_XL, q8 kv:

策略10k25k50k75k
Q4/q87/108/105/104/10
Q6->Q6/q8->Q4->Q4/q87/107/107/107/10

结果:动态量化在超过50k上下文时胜过静态,在此前基本持平。

总体:

在这里,我直接将动态策略与参考模型进行比较,去掉了10k和25k任务,因为在低于这些级别时,动态策略实际上就是在运行Q6/f16。它们每次都是相同的。

策略50k75k
Q6->Q4->Q37/104/10
Q6->Q6/q8->Q4->Q4/q87/107/10
Q6/f166/108/10

结果:我认为这些结果没有太多可借鉴的,除了IQ3_S量化在长上下文下确实崩溃。这张表说明了我为什么没有太把任务正确性当回事。从字面上看,它表明Q6->Q4和Q6->Q6/q8在50-75k上下文之间优于Q6/f16!

结论

总的来说,我对这些结果非常满意!

虽然这个基准测试并不完美,但就我的目的而言,我非常满意动态模型量化是抵消硬件限制带来的典型精度损失的好方法。

我的测试主要围绕推动24 GiB GPU的极限,因为这样更容易与只能在32 GiB GPU上运行的参考模型进行比较。作为下一步,我打算将其集成到我的推理设置中,看看在启用所有附加功能(即投机解码和mmproj,这两者在这些基准测试期间均未启用)时,它的表现如何。

我的直觉是,在将这些策略用于实际推理时,需要把握好的权衡是避免过于频繁地降低模型量化级别。虽然我认为连贯性没问题,但换出权重所需的时间在某一点上会变得明显。所以,我想我会尝试设置一个方案,创建大的上下文“区块”,让模型在大约4-5万token内保持不变地运行。

我认为Q6/f16->Q6/q8->Q4/f16->Q4/q8策略已经是一个很好的例子。至少在我目前对llama.cpp的修改下,kvcache重载比模型重载花费的时间要少得多。也许我将来能在这方面继续努力!

―― 高票评论(帖子共22条讨论)――

[+23] u/Dany0: ....

....

哦我的天,我想你意外发现了OpenAI/Anthro如何量化它们的模型。这就是它。这就是大家一定都看到的东西。很明显,他们“哦,我们总是用fp16”的说法一定不实,但与此同时,那些在opus/gpt上重新运行基准的追踪网站显示,并没有观察到真正的差异。

这就是大家一定都看到的情况。并不是他们一开始就提供量化模型,而是随着上下文增长,你的请求最终会被分流到更量化的版本。这也解释了随机的TTFT增加和连接断开。

[+3] u/vacon04: 这是一个有趣的方法。我认为对于拥有“普通”消费级机器的人来说,主要的缺点是你在最初就需要一个大的模型量化,并且在前n个token还需要一个大的KV缓存。这意味着如果你的显存受限,甚至尝试一下都变得不可行。

[+2] u/Top-Evidence174: 哈,这还真是巧妙。从没想过在运行中这样更换量化版本。

[+1] u/Wooden_Jelly_5295: 希望下一个是工具使用基准。找到针是一回事;继续缝纫又是另一回事。

[+1] u/Miserable-Dare5090: 这在类似dwarfstar的引擎上会非常有趣。因为它有非常好的缓存和会话存储。我在想,是否有可能在那个引擎中使用同一个缓存在模型之间切换?

[+1] u/Open-Adhesiveness-86: 在llama.cpp上,你可以通过slot保存/恢复(/slots/{id}?action=save)实现大部分功能,因为状态文件并不记录是哪个模型写入的。不过dtype和布局必须一致,用fp16 K/V保存,然后加载到运行q8_0缓存构建时,你会得到静默的垃圾数据,而不是错误。

阅读原文
📚 相关主题 大语言模型

订阅 AI Pulse

每天 08:00 · 12:30 · 18:30 · 23:50 更新