引言

推理加速的核心,是把原本固定且重复的推理路径变成输入相关的动态路径。性能收益来自少算、并行、复用与跳过,但风险是把内部控制流与资源足迹外显成可观测信号。一旦攻击者能观测到这些信号,就可能在不接触明文的情况下推断提示词特征,甚至恢复部分内容。当今,MoE在业界被频繁采用,原因是它把规模与算力之间的矛盾拆开:模型参数可以继续膨胀,但每个Token只激活少量专家,从而把单步计算压到可控区间。同时,在引入了这些相关架构之后,也无形的引入了新的风险。

SpeculativeDecoding侧信道

SpeculativeDecoding的主流实现是草稿模型先一次性起草一段候选token,再由目标模型并行验证,验证到首次不一致处即停止接受,随后继续下一轮。这个过程把原本每次只生成一个token的固定节奏,改造成每轮接受多个token且接受长度与输入相关的动态节奏。SpeculativeDecoding最初以不改变目标模型输出分布为前提,通过并行化验证来加速自回归解码,

介绍

在传统自回归解码中,模型每生成1个token就必须完成1次完整前向计算,生成K个token需要串行执行K次前向,延迟与吞吐很容易被这条串行链路锁死。SpeculativeDecoding的工程价值在于把多步token生成折叠进一次或少数几次目标模型前向里:先用更快的草稿模型提出一段候选token前缀,再让目标模型在一次前向中并行验证这段候选,能接受多少就一次性提交多少,从而显著降低平均每token的等待成本。

从落地形态看,推理框架往往把它实现为两条执行路径:DraftRunner负责起草,TargetRunner负责验证,并把它与连续批处理调度、KV-cache内存管理绑定在一起,形成可配置的生产级加速模块。

原理

SpeculativeDecoding之所以能在原则上保持与目标模型一致的输出分布,关键在于验证阶段的拒绝采样思想:草稿模型只是提出候选,是否接受由目标模型的概率分布决定。一旦出现不一致,就在目标模型分布下进行修正采样并进入下一轮。也因此,正确的实现需要把接受逻辑设计成严格的逐token验证与拒绝采样过程,而不是简单的相等比较。

被接受的草稿token占草稿生成token的比例越高,平均每轮能提交的token越多,端到端加速越明显。极端情况下如果全部被拒绝,退化成几乎只由目标模型生成单token

工作机制

通过解码的视角,把主流draft与target双模型SpeculativeDecoding机制拆开看,主要有四个阶段

  1. 草稿阶段:给定当前前缀x₍<t₎,草稿模型以较低成本连续起草k个候选token,得到y₁…y_k。
  2. 并行验证阶段:把前缀与候选token拼接后送入目标模型,用一次或少数几次前向并行计算这k个位置的条件分布,并对候选token逐个做验证与接受判定。
  3. 首次不一致即截断:一旦在位置j出现不一致,接受段落只保留到j-1,并对位置j在目标模型分布下进行修正采样,然后结束本轮,进入下一轮起草。该接受到首次不一致即停止的行为,在工程实现中非常常见,也是节奏从固定单token变成输入相关动态多token的直接原因。
  4. 进入下一轮 新一轮从更新后的前缀继续,重复上述流程。vLLM等框架进一步把这一轮级别的行为塞进批处理调度器,使多请求并发时仍能维持吞吐。

把输出token看成按轮提交的块状流的话:

  • 传统自回归:每轮提交1个token,轮次数≈输出token数
  • SpeculativeDecoding:每轮提交0到k个token,轮次数≈输出token数除以每轮平均接受长度

这条轮次数与每轮接受长度的波动,就是后面侧信道的主要载体。

观测特征

如果攻击者无法看到明文内容,但能看到推理服务的外显元数据,SpeculativeDecoding会额外暴露一组传统自回归没有或较弱的可学习信号。

最常用的特征包括:

  1. 每轮提交token数序列 也可等价为每轮接受长度序列,比如一段输出对应的序列可能是[3,3,1,0,2,…]。该序列对输入提示具有独特且可复现的指纹特征,可用于区分不同提示与模板。
  2. 轮间停顿与token突发 当一轮接受多个token时,客户端往往表现为短时间连续收到多个token,随后出现轮间等待。轮间等待长度与草稿起草、目标并行验证、首次不一致位置高度相关。
  3. 网络包大小与包间隔 即使TLS加密了内容,包大小与到达时间仍可被被动观察者获取正确推测越多token,每轮需要传输的token越多,包大小越大,导致仅通过包大小变化就能推断每轮提交token数。
  4. 输出总轮次数与整体生成时间 在同样输出长度下,轮次数越多通常意味着接受率越低,提示越难或越偏离草稿模型擅长分布,而这类宏观统计量也能成为分类器特征。

SpeculativeDecoding为何为造成侧信道泄漏

输入到speculation pattern的映射是强相关的。正确与错误推测的模式依赖输入,并且这种输入相关模式能从外部可观测的包大小与生成节奏变化中被推断出来。

SpeculativeDecoding把这种模式从内部状态外显化了。传统自回归解码的外显节奏更接近等步长单token流,攻击者更难仅凭节奏恢复输入信息。而SpeculativeDecoding把每轮接受长度变成输入相关的随机变量,泄漏一下路径:

  • 输入影响草稿模型命中率
  • 命中率决定首次不一致位置与每轮接受长度
  • 接受长度决定轮次数、token突发形态、包大小
  • 攻击者对这些元数据做序列建模即可得到输入指纹

因此它不是单纯的计时抖动问题,而是系统为了性能引入的数据依赖控制流与数据依赖通信形态,属于典型侧信道设计悖论,把加速的内部决策暴露成可观测的外部节律。

攻击手法

查询指纹与模板匹配

攻击者通过把每轮接受token数的轨迹trace看作时间序列信号,构建模板库或训练分类器。利用了SpeculativeDecoding两个特点:

  1. SpeculativeDecoding的接受与回退机制天然产生离散而强约束的形状特征:正确推测时一轮最多可验证并接受到某个上限n个token,错误推测时退化为单token推进,这使得trace同时具有上界形态与大量1值段落,形成强指纹。
  2. trace并非纯噪声,而是输入依赖的可学习信号。

模板匹配的风险在于它更贴近提示隐私泄漏:一旦候选集合有限,例如企业内常见的工单模板、固定的检索查询、医疗问诊的常见问题集,命中式识别会把数据泄漏升级为对具体提示的恢复能力。

代码示例

def build_template_library(
    client: SpeculativeModelClient,  # 生成客户端
    prompts: List[str],  
    runs: int = 3,  
    max_new_tokens: int = 64,  
    speculative_k: int = 4,  
) -> Dict[str, List[float]]:
    library: Dict[str, List[float]] = {}  # 模板库
    for prompt in prompts:  # 遍历提示
        seqs: List[List[int]] = []  # 收集轨迹
        for _ in range(runs):  # 重复采样
            trace = client.generate(
                prompt,  
                max_new_tokens=max_new_tokens,  
                speculative_k=speculative_k, 
                use_speculative=True,  # 使用推测解码
            )
            seqs.append(extract_shape(trace))  # 存入轨迹
        library[prompt] = average_sequence(seqs)  # 生成模板
    return library 

def match_prompt(
    client: SpeculativeModelClient,  # 生成客户端
    library: Dict[str, List[float]],  
    unknown_prompt: str,  
    max_new_tokens: int = 64,  
    speculative_k: int = 4,  
) -> List[Tuple[str, float]]:
    trace = client.generate(
        unknown_prompt,  # 未知提示
        max_new_tokens=max_new_tokens,  
        speculative_k=speculative_k,  # 推测窗口
        use_speculative=True,  # 使用推测解码
    )
    target_shape = [float(x) for x in extract_shape(trace)]  # 获取目标轨迹
    ranking: List[Tuple[str, float]] = []  # 排序结果
    for prompt, template in library.items():  # 遍历模板
        score = shape_distance(template, target_shape)  # 计算距离
        ranking.append((prompt, score))  # 记录结果
    ranking.sort(key=lambda x: x[1])  # 按距离排序
    return ranking  

通过对每个候选提示重复采样accepted序列,长度不足处用末尾值补齐后求均值作为模板。对未知提示获取accepted序列,计算模板与目标的对齐L1距离并排序,利用推测解码的接受和拒绝形状可复现性实现命中式指纹识别

超参数反推

trace在推测时每轮最多可验证到n个token,错误推测时每轮仅生成1个token,因此trace的最大值边界与分布形态天然携带n的影子,进而可用于推断推测窗口相关参数。在vLLM等主流框架中,num_speculative_tokens被定义为每个解码步骤中推测token数量的上限,这意味着只要外部能稳定观测到每轮接受token数的上界形态,就存在把观测边界映射回配置参数的大致范围的攻击面。

一旦攻击者能判断是否启用SpeculativeDecoding、可能的推测窗口范围、甚至偏向哪类推测方案,就能更有效地选择后续黑盒推断策略或放大差异的交互式探测策略。

伪代码

def infer_spec_params(trace: GenerationTrace) -> Dict[str, float]:
    max_accept = max((len(s.accepted) for s in trace.steps), default=0)  # 最大接受长度
    reject_steps = sum(1 for s in trace.steps if s.rejected)  # 拒绝步计数
    total_steps = max(len(trace.steps), 1)  # 总步数
    reject_ratio = reject_steps / total_steps  # 拒绝率
    mean_accept = sum(len(s.accepted) for s in trace.steps) / total_steps  # 平均接受
    speculative_enabled = max_accept > 1 or reject_steps > 0  
    window_guess = max(1, int(max_accept * 1.2))  
    return {
        "推测解码可能启用": float(speculative_enabled), 
        "推测窗口估计": float(window_guess),  
        "拒绝率": reject_ratio,  
        "平均接受长度": mean_accept,  
    }

通过利用accepted最大长度、拒绝分布等轨迹形状可反推出测窗口上限、是否启用SpeculativeDecoding及相关配置等。

流量分析

即使内容被TLS加密,攻击者仍能看到包大小与到达节奏等元数据。SpeculativeDecoding的关键泄漏点在于,每轮迭代被接受的token数不是常量,而是由输入决定的正确推测与错误推测模式所驱动;正确推测时一轮会产生更多token,从而对应更大的网络包,错误推测时往往退化到一轮仅产生1个token,包也更小。若果传统网站指纹的思路,核心不是解密内容,而是做序列分类:用包大小序列与分组节奏构造指纹,再对未知会话做匹配或分类。

def extract_signal(trace: GenerationTrace, tokenizer) -> Dict[str, List[int]]:
    """接受token数轨迹与模拟包大小序列"""
    accepted_counts = trace.accepted_counts  
    packet_sizes: List[int] = []  # 初始化包大小列表
    for step in trace.steps:  # 遍历每步轨迹
        chunk_text = tokenizer.decode(step.accepted, skip_special_tokens=True)  
        packet_sizes.append(len(chunk_text.encode("utf-8")))  
    return {"accepted_counts": accepted_counts, "packet_sizes": packet_sizes}  # 返回信号字典

def build_fingerprint(signal: Dict[str, List[int]]) -> str:
    """把轨迹压缩成指纹字符串"""
    merged: List[int] = []  # 初始化合并序列
    merged.extend(signal["accepted_counts"])  # 追加接受序列
    merged.extend(signal["packet_sizes"])  # 追加包大小序列
    series = ",".join(str(x) for x in merged)  # 序列转字符串
    return hashlib.sha256(series.encode()).hexdigest()  

将SpeculativeDecoding的草稿生成与目标模型验证形成的accept与reject轨迹视作可观测信号,草稿提出的多token在验证后被部分接受,导致每步accepted长度和分片大小随提示内容变化且可重复。攻击者多次采样同一提示,获取accepted长度序列和估算的包大小序列,离线构建指纹库。

语义属性推断与用户画像侧写

当攻击者拿不到完整候选提示集合,或者提示空间过大难以做精确命中时,攻击目标会自然下沉为语义属性推断:只要能从trace或包大小序列里区分不同类别,就能形成对用户会话主题的侧写。更广义地看,属性推断并不只依赖包大小,也可依赖时间。

高效推理技术会引入数据相关的推理时延特征,攻击者通过观察加密网络流量中的快慢差异即可学习会话内容的有限信息,例如对话主题类别,并且在更强假设下甚至可恢复提示中的PII。

def extract_stats(trace: GenerationTrace) -> Dict[str, float]:
    accepts = trace.accepted_counts  # 获取接受序列
    latencies = trace.step_latencies  # 获取时延序列
    if not accepts:  
        return {"mean_accept": 0.0, "burst_ratio": 0.0, "mean_latency_ms": 0.0}  # 返回零特征
    mean_accept = sum(accepts) / len(accepts) 
    burst_ratio = sum(1 for x in accepts if x > 1) / len(accepts)  
    mean_latency = sum(latencies) / max(len(latencies), 1)  
    return {
        "mean_accept": mean_accept,  
        "burst_ratio": burst_ratio,  
        "mean_latency_ms": mean_latency,  
    }

def feature_distance(a: Dict[str, float], b: Dict[str, float]) -> float:
    return (
        abs(a["mean_accept"] - b["mean_accept"])  # 平均接受差
        + abs(a["burst_ratio"] - b["burst_ratio"])  # burst差
        + abs(a["mean_latency_ms"] - b["mean_latency_ms"]) / 100.0  # 时延差缩放
    )

def build_category_templates(
    client: SpeculativeModelClient,  
    labeled_prompts: Dict[str, List[str]], 
    max_new_tokens: int = 64,  
    speculative_k: int = 4,  
) -> Dict[str, Dict[str, float]]:
    templates: Dict[str, Dict[str, float]] = {} 
    for category, prompts in labeled_prompts.items():  
        feats: List[Dict[str, float]] = []  
        for prompt in prompts:  # 遍历提示
            trace = client.generate(
                prompt,  
                max_new_tokens=max_new_tokens,  
                speculative_k=speculative_k,  
                use_speculative=True,  # 使用推测解码
            )
            feats.append(extract_stats(trace))  
        mean_accept = sum(f["mean_accept"] for f in feats) / len(feats)  # 平均接受
        burst_ratio = sum(f["burst_ratio"] for f in feats) / len(feats)  # 平均burst
        mean_latency = sum(f["mean_latency_ms"] for f in feats) / len(feats)  # 平均时延
        templates[category] = {
            "mean_accept": mean_accept,  # 类别平均接受
            "burst_ratio": burst_ratio,  # 类别burst
            "mean_latency_ms": mean_latency,  # 类别时延
        }
    return templates  

攻击者把SpeculativeDecoding的接受节奏当作语义侧写信号,草稿模型一次提出多token、目标模型逐步验收会在不同语义下呈现不同的平均接受长度、burst比例与步时延分布。为已知类别先采样这些统计量形成类别至原型特征,再对未知提示提取同样特征以L1距离找到最近类别。

时序侧信道与会话属性推断

高效推理让模型对不同输入表现出不同快慢,攻击者只需观察响应更快还是更慢,就能学习与内容相关的信息。SpeculativeDecoding会引入数据相关的时延特征,攻击者通过监测受害者与远程模型之间的加密网络流量,依据响应快慢即可推断消息内容的部分信息。与包大小指纹相比,时序侧信道在工程上更难彻底消除,因为它不仅来自输出分组策略,也来自推理路径本身的计算量差异。

def collect_timings(
    client: SpeculativeModelClient,  
    prompt: str,  
    runs: int = 3,  
    max_new_tokens: int = 64,  
    speculative_k: int = 4,  
) -> Dict[str, float]:
    totals: List[float] = []  # 总时延列表
    gaps: List[float] = []  # 平均步间隔列表
    for _ in range(runs):  
        trace = client.generate(
            prompt,  
            max_new_tokens=max_new_tokens,  
            speculative_k=speculative_k,  
            use_speculative=True,  
        )
        totals.append(trace.total_latency_ms)  # 记录总时延
        if trace.step_latencies:  # 若有步时延
            gaps.append(statistics.mean(trace.step_latencies))  # 记录平均间隔
    mean_total = statistics.mean(totals) if totals else 0.0  # 计算平均总时延
    std_total = statistics.pstdev(totals) if len(totals) > 1 else 0.0  # 计算总体标准差
    mean_gap = statistics.mean(gaps) if gaps else 0.0  # 计算平均步间隔
    return {"mean_total_ms": mean_total, "std_total_ms": std_total, "mean_gap_ms": mean_gap}  

对每个提示多次生成trace,记录total_latency_ms、step均值间隔及方差,作为时序指纹。对未知提示同样采样后按加权L1距离与库对比排序,利用SpeculativeDecoding引入的输入相关时延差异实现远程时序侧信道推断。

防御

防御的核心目标是把输入相关的speculation pattern从外显元数据中抹平,或至少把可学习信号的分辨率降到不可用。通过把响应包填充到固定或近固定大小,降低包大小对每轮token数的泄漏强度。同时对高敏感业务关闭SpeculativeDecoding或改用更稳定的解码路径,把性能收益留给低敏感与高并发场景。

MoE路由侧信道

介绍

MoE(Mixture-of-Experts)的核心价值是用稀疏激活把模型容量做大、把单次推理计算做小:每个Token只会被路由到少数几个专家网络执行,而不是像稠密模型那样让所有参数都参与计算。这个设计在工程上通常伴随两类优化:一类是路由与专家并行带来的批处理与调度优化,另一类是为了吞吐而引入的容量控制与丢弃机制,例如当某个专家的容量超限时对溢出的Token进行丢弃或重路由。它们共同决定了一个事实:MoE的执行路径对输入高度敏感,且这种敏感性会被映射为可观测的时间与资源足迹,从而天然形成侧信道。

在真实的LLM服务里,侧信道不一定来自模型输出本身,更多来自推理系统的可观测面:端到端延迟的微小抖动、单Token生成节奏等。MoE把输入语义映射为专家选择与负载分布,这相当于把语义信息编码进了这些可观测面里。

原理

从攻击视角看,MoE侧信道的本质是输入相关的条件计算路径泄漏。

  1. 路由决策是输入相关的 路由器通常对每个Token计算一组分数,然后选择Top-k专家,并把Token派发到对应专家执行。分数与Top-k结果与Token语义强相关,因此专家ID序列、每层每Token的Top-k组合、专家负载分布都隐含了输入信息。
  2. 专家执行会产生输入相关的资源足迹 不同专家的Token数量不同会导致不同的矩阵乘与访存强度,叠加并行与融合算子后,会在CPU或GPU侧呈现为时间差、Cache占用差、TLB行为差、性能计数器差等。
  3. 容量与丢弃机制会引入跨请求耦合 当专家容量不足触发Token dropping时,一个用户的Token是否被丢弃不仅取决于自己,还会被同批次其他用户的Token竞争所影响,这会形成跨批次侧信道甚至变成可控的信息通道。

工作机制

从攻击视角来看,可以把一次MoE推理拆成可观测的环节。不同实现细节会不同,但大致一致:

  1. Token级路由计算,路由器对每个Token计算门控分数,选择Top-k专家,并生成派发表。这里的可攻击面包括:
  • 每层Top-k专家的分布是否稳定
  • 特定Token是否总倾向某些专家
  • Top-k分数边界附近的抖动是否导致路由翻转

这些现象决定了后续所有资源足迹。

  1. Dispatch与专家缓冲区填充,系统把同一专家的Token聚合成连续块送入专家网络,并为每个专家维护容量上限与缓冲区。攻击面包括:
  • 每个专家实际收到的Token数
  • 是否触发容量上限
  • 丢弃或重路由发生的频率与位置

把容量超限下的Token dropping与路由细节结合起来,可以发现可被利用的跨批次依赖。

  1. 专家前向计算与合并。专家网络执行后再把结果合并回原序列位置。这一阶段可以发现
  • 专家执行的先后与并行度
  • 每轮执行耗时与访存节奏
  • 合并阶段的同步点是否被某些专家拖慢

Moe为何为造成测信道泄漏

把MoE与稠密模型对比,MoE更容易产生侧信道泄漏的原因通常集中在四点:

  1. 稀疏激活放大了可区分性:稠密模型每步计算相对固定,输入差异更多体现在数值上,不容易外显为明显的执行分岔。MoE则把语义差异离散化为专家选择差异,导致执行足迹可分性显著增强。
  2. 专家负载不均带来可测的时间与资源波动:同一层里,不同Token被分配到不同专家,某些专家瞬时负载高就会更慢或更占资源。
  3. 容量控制与Token dropping引入跨请求信息通道:当容量超限出现丢弃时,一个请求的输出质量与丢弃模式会被同批次其他请求影响,这相当于把多用户请求耦合在同一个共享资源上。
  4. 工程实现更依赖共享硬件与系统优化:MoE为了省算力,常用专家并行、跨卡派发、融合算子与缓存复用。共享越多,可观测面越多

攻击手法

提示词内容推断

在不做全文逐Token还原的前提下,恢复提示中的关键词集合命名实体或结构片段,例如是否包含某个产品名、漏洞编号、客户名称,或提示是否属于某种固定模板.

攻击思路

  • 不做全文重建,只做候选集判别,对候选关键词存在性检测、候选模板识别、弱位置推断。
  • 建立足迹序列与候选集的对应:同一候选词或同一模板在路由触发上更可能诱发相似的专家组合与负载分布,从而在侧信道特征上形成可学习的边界。
def detect_keywords(prompt: str) -> List[Dict[str, object]]:  # 多标签探测主函数
    baseline = _side_channel_trace(prompt, stage="baseline")  # 获取基线特征
    findings = []  
    for candidate in CANDIDATES:  # 遍历候选关键词
        probe_prompt = f"{prompt}\n\n#probe:{candidate['keyword']}"  
        probe_trace = _side_channel_trace(probe_prompt, stage=f"probe:{candidate['keyword']}")  
        delta_cache = probe_trace["cache_proxy"] - baseline["cache_proxy"]  
        delta_tlb = probe_trace["tlb_proxy"] - baseline["tlb_proxy"]  
        delta_perf = probe_trace["perf_counter"] - baseline["perf_counter"]  
        delta_page = probe_trace["page_reload"] - baseline["page_reload"] 
        delta_load = probe_trace["stats"]["top2"] - baseline["stats"]["top2"] 
        delta_entropy = probe_trace["stats"]["seq_entropy"] - baseline["stats"]["seq_entropy"]  

        score = max(  
            delta_cache * 0.25 + delta_tlb * 0.25 + delta_perf * 0.2 + delta_page * 0.2,
            delta_load * 0.3 + delta_entropy * 0.3,
        )  # 得分计算
        present = score > 0.05  # 判定是否存在关键词

        findings.append(  
            {
                "keyword": candidate["keyword"], 
                "note": candidate["note"],  
                "score": round(score, 3),  
                "present": present,  
                "delta_cache": round(delta_cache, 3),  
                "delta_tlb": round(delta_tlb, 3),  
                "delta_perf": round(delta_perf, 3),  
                "delta_page": round(delta_page, 3),  
                "delta_top2": round(delta_load, 3),  
                "delta_seq_entropy": round(delta_entropy, 3),  
            }
        )  
    return findings  # 返回探测结果

相比全文重建,这类攻击更容易在现实中成功,因为它只需要稳定区分少量候选,不要求高精度逐token还原。其防御难点在于业务侧候选集往往高度固定且复用频繁,给了攻击者足够的先验与训练数据窗口。

响应重建

恢复模型对受害者请求生成的响应内容,或至少恢复响应中的关键片段与敏感实体

利用思路:decode是逐Token生成,在每一步生成的新token都会改变上下文,从而改变下一步的路由决策与负载。这使得执行足迹更像一段可被解码的序列攻击者可以把每步的专家序列S或负载L当作观测量,去预测该步输出token或其类别

def reconstruct_response(prompt: str) -> Dict[str, object]:  # 重建响应
    start = time.perf_counter()  # 记录开始时间
    data = run_generation_with_scores(prompt, max_new_tokens=64, temperature=0.8, top_p=0.9)  # 生成并获取分数
    elapsed_ms = (time.perf_counter() - start) * 1000  #

    gen_tokens = data["tokens"][data["prompt_token_count"] :]  # 提取生成token
    logprobs = data["logprobs"]  # 提取logprob
    step_time = elapsed_ms / max(len(gen_tokens), 1)  # 单步时间

    seq_proxy = _simulate_seq(int(start * 1e6), len(gen_tokens) or 1)  # 模拟decode专家序列
    proxy = _side_channel_proxy(prompt, elapsed_ms, seq_proxy)  #

    timeline: List[Dict[str, object]] = []  
    recovered_plain = ""  
    for idx, token in enumerate(gen_tokens):  # 遍历生成token
        clean = token.replace("▁", " ").strip()  # 清理token前缀
        recovered_plain += clean  
        token_logprob = logprobs[idx] if idx < len(logprobs) else None  # 获取对应logprob
        timeline.append(  
            {
                "t_ms": round(step_time * (idx + 1), 2),  
                "token": token,  
                "clean": clean,  
                "logprob": round(token_logprob, 3) if token_logprob is not None else None,  
                "expert_id": seq_proxy[idx] if idx < len(seq_proxy) else None,  
            }
        )  

    return {  
        "prompt": prompt,  
        "elapsed_ms": round(elapsed_ms, 2),  
        "proxy": proxy,  
        "timeline": timeline,  
        "recovered_text": recovered_plain,  
        "model_response": data["response"],  
    }  

路由指纹识别

不追求逐Token恢复全文提示,而是识别提示的类别与属性,常见包括是否涉及医疗金融身份信息,是否出现企业项目代号,是否为代码生成代码审计等高价值任务。

攻击方法

把路由指纹识别落地成一条可复现的链路,通常是四步:

  1. 收集不同任务类型的样本提示,分别采集侧信道trace。
  2. 把trace转换为L或S的近似表示,或直接提取能代表route形状的特征
  3. 监督分类或度量学习,得到任务类型与属性的识别器
  4. 对未知受害者请求采集trace并输出类别与敏感属性置信度
def _side_channel_trace(prompt: str):   
    start = time.perf_counter()  # 记录开始时间
    result = run_generation(prompt, max_new_tokens=48, temperature=0.6, top_p=0.95)  
    elapsed_ms = (time.perf_counter() - start) * 1000  # 计算延迟毫秒

    seed = int(hashlib.sha256(prompt.encode()).hexdigest(), 16)  # 以提示哈希生成种子
    rng = random.Random(seed)  # 创建可重复随机源

    cache_proxy = 0.4 + rng.uniform(0.1, 0.4)  
    page_reload = 0.3 + rng.uniform(0.05, 0.3)  
    perf_counter = 0.35 + rng.uniform(0.1, 0.35) 
    tlb_reload = 0.28 + rng.uniform(0.05, 0.3) 

    load_vector = _simulate_expert_load(seed)  
    decode_seq = _simulate_expert_sequence(seed, length=18)  
    stats = _summarize_load(load_vector, decode_seq, elapsed_ms)  # 提取统计特征

    trace = {  # 侧信道代理与统计特征
        "latency_ms": round(elapsed_ms, 2),  
        "cache_proxy": round(cache_proxy, 3),  
        "page_reload": round(page_reload, 3),  
        "perf_counter": round(perf_counter, 3),  
        "tlb_reload": round(tlb_reload, 3),  
        "load_vector": load_vector,  
        "decode_seq": decode_seq,  
        "stats": stats,  
    }  
    return trace, result  

这种攻击对细粒度内容不敏感,但对业务同样致命,因为任务类型本身往往就是敏感信息。尤其在多租户推理与共享硬件场景,指纹一旦可复现,就可能形成长期可追踪的用户画像与业务态势泄露。

安全路由诱导

这里不以隐私泄露为主,而是触发更不安全的route,更像越狱攻击。通过使模型在相同安全策略下更容易输出有害内容,从而造成大模型越狱。主要利用了route由输入驱动且不同route上的安全性不一致。

攻击思路

  1. 将越狱目标从文本层绕过扩展到路由层失配
  • 文本层越狱强调提示工程对齐失败,可以用到元指令劫持。
  • 路由层诱导强调结构性脆弱点,在稀疏路径上安全训练与对齐覆盖不足,导致同一问题在不同route下安全边界不同。
  1. 触发特定路由配置的风险点
  • 当路由对某些语义簇高度敏感时,输入的表面改写可能引发专家选择序列突变,从而进入更不安全的路径。
  • 在多层Top-k路由堆叠下,跨层组合空间巨大,unsafe route更可能以稀疏形式存在,难以靠常规对齐训练覆盖。

它不必依赖高精度侧信道采样,即使没有硬件观测能力,只需通过在文本层,攻击者也可能通过黑盒试探找到更易出有害输出的route区域。更进一步,而一旦攻击者同时具备侧信道观测能力,便可能把试探过程加速为更有效的搜索与归因。

防御

MoE侧信道的防御应优先围绕三条主线做闭环治理:

  1. 打断跨用户耦合,默认对不同租户与敏感请求做强隔离,尽量取消跨用户混批或按安全域分批,并避免专家容量边界被不同用户共享导致的TokenDropping泄漏面。
  2. 削弱可观测信号,将路由与调度的输入相关性做去信息化处理,例如稳定化容量管理与回退策略、减少或消除丢弃引入的可区分行为、对执行与调度引入随机化与抖动,并对流式输出做节奏整形,从而降低从时间序列与资源足迹反推route与负载的可分性
  3. 同时把route纳入安全评估与对齐训练测试,专门检查是否存在unsafe routes与路由触发的安全性波动,并把检测、告警、回滚与持续监控纳入治理流程。

参考

https://arxiv.org/pdf/2211.17192

https://blog.vllm.ai/2024/10/17/spec-decode.html

https://developer.nvidia.com/blog/an-introduction-to-speculative-decoding-for-reducing-latency-in-ai-inference/

https://arxiv.org/abs/2507.17033

https://arxiv.org/abs/2410.22884

https://arxiv.org/abs/2508.15036

https://openreview.net/forum?id=JRtldw5Mpw