Prhub

#28723 [Intel XPU] Enable fused_moe_triton tuning on XPU and add tuned DeepSeek-OCR-2 configs

原始 PR 作者 rahulvijayaraghavan 合并时间 2026-09-16 10:20 文件变更 7 提交数 4 评论 8 代码增减 +433 / -26

执行摘要

让 fused_moe_triton 调优器支持 XPU,并新增 Arc Pro B60 的 DeepSeek-OCR-2 MoE 调优配置

动机来自 PR body:调优器 _sep 版本是 CUDA 专用的,图捕获、事件、播种和计时全部直连 torch.cuda.*,且 Ray worker 把每个 actor 都钉在 device 0 上,导致 XPU 上无法完成有意义的 MoE 调优。作者希望按 Intel Triton 团队文档推荐的写法(TensorDescriptor + autotune)在 XPU 上把 fused_moe_triton 调到最优,并为 Arc Pro B60 上的 DeepSeek-OCR-2 留下可直接复用的配置数据。过程还暴露并推动了两个上游 Triton issue:intel/intel-xpu-backend-for-triton#7306(host 侧 TensorDescriptor 丢掉 2D-block-I/O 快路径)与 #7691(TensorDescriptor 形状特化导致每个 batch size 编译一次),两者均已关闭。

值得精读,尤其是正在做多后端 Triton kernel 调优或维护 MoE 相关 benchmark 工具的工程师。三个值得关注的设计决策:一是用 get_device_module 一次性收敛同步、事件、图捕获与播种,避免在 kernel 里写 XPU 专用分支,这是「最小持续成本」路线的具体落地;二是 clear_b_tma_desc_cache 揭示的隐藏耦合——描述符缓存与图捕获内存争用,在长生命周期服务的语境下为什么是 no-op;三是把 topk_ids 层布局从硬编码改为从模型配置解析,代价是引入了对模型元数据字段的隐式依赖。阅读时建议同时对照 Intel Triton issue #7306 / #7691 与 PR body 中的 workload 结果表,并留意 tuning_fused_moe_triton.py 与 _sep 版本仍需保持行为一致,以及 review 中 airMeng 提出的下一步方向(KV cache store 等 memory-bound kernel)。

讨论亮点
  • 路线投入之争(design):mingfeima 明确表态「our strategy is to keep triton path functional not necessarily performant」,认为 XPU 上 Triton 只是 nice to have,应保持最小投入;airMeng 补充「I am okay for tuning triton configs since it can improve performance with the least effort. But DeepSeek-OCR is not quite critical. You can switch to some other memory-bound triton kernels like kv cache store」,即认可调 config 的性价比但质疑所选场景的重要性。作者引用 Intel Triton 官方「Improving performance of your Triton source code」指南回应,并强调持续成本接近零:不改 MoE kernel 源码、无 XPU 专用分支、tuner 改动是设备无关的 bug 修复。
  • CI 回归(testing):mingfeima 以 CHANGES_REQUESTED 指出 benchmark/kernels/fused_moe_triton/common_utils.py 新增 num_layers / dense_layers 后未同步 test_fused_moe_common_utils.py,Kimi-VL 期望字典缺两个字段导致 base-a-test-cpu 失败,并要求「Need update the existing assertion and add coverage for the DeepSeek-OCR layer layout used by load_topk_ids」。作者在 3a37051cb1 修复并补齐 DeepSeek-OCR 覆盖;mingfeima 仍留下「Please do self check next time. I am super busy right now...」。
  • 搜索空间截断(correctness):gemini-code-assist 对 tuning_fused_moe_triton.py 中 search_space = search_space[:4] 打出 high priority,要求合并前移除。最终 configs 覆盖 18 个 batch size 的全量 sweep,说明截断未进入最终版本。
  • 设备绑定与缓存副作用:commit message 与 PR body 记录了 XPU 上 Ray mask 生效过晚导致所有 worker 落在 device 0,以及 B 描述符缓存钉住死权重、饿死图捕获两个非显而易见的坑,前者用 ray.get_gpu_ids() 修复,后者用 clear_b_tma_desc_cache() 修复。

实现拆解

  1. 设备抽象(tuning_fused_moe_triton_sep.py):新增 get_device / get_device_module / is_xpu 导入与模块级 device_module、_is_xpu;KernelWrapper.cuda_graph_wrapper 与 forward_cost 中的 torch.cuda.synchronize、torch.cuda.Event、torch.cuda.CUDAGraph、torch.cuda.graph 全部替换为 device_module.*,XPU 分支改用 torch.xpu.XPUGraph 作为图类。BenchmarkWorker.__init__ 的 torch.set_default_device("cuda") 改为 get_device(),manual_seed_all 同样走设备模块;benchmark / tune(Round 1 与 Round 2)/ cmp_configs 中的设备上下文条件由 is_hip() 放宽为 _is_xpu or _is_hip,使 XPU 也能正确切换 device。改动方式刻意对齐同目录已有的 tuning_fused_moe_triton.py,保持两条工具路径一致。
  2. Ray worker 绑定设备(tuning_fused_moe_triton_sep.py、pyproject_xpu.toml):self.device_id 由固定 0 改为 0 if not ray.is_initialized() else int(ray.get_gpu_ids()[0])。commit message 说明原因是 XPU 上 Ray 的可见设备掩码生效太晚,若不显式使用 Ray 分配的 id,每个 worker 都会跑在 device 0 上。配套在 pyproject_xpu.toml 新增可选 extra ray = ["ray[default]>=2.55.1", "dpctl"],注释指出 dpctl 是 Ray 注册 Intel GPU 资源的前提;该 extra 为 opt-in、不并入 all,因此不影响服务化安装。
  3. 层布局改为配置驱动(tuning_fused_moe_triton_sep.py、benchmark/kernels/fused_moe_triton/common_utils.py):load_topk_ids 删掉硬编码的 num_layers = 61 / dense_layers = 3,改为通过 sglang.srt.runtime_context 的 get_model() / get_parallel() 拿到模型路径、tp/ep,再调用 get_model_config,用返回的 num_layers、dense_layers 推导 MoE 层数。为此 get_model_config 把 DeepseekOCRForCausalLM 加入 DeepSeek 家族分支,并在返回值中新增 num_layers(getattr(config, "num_hidden_layers", 0))与 dense_layers(getattr(config, "first_k_dense_replace", 0),注释说明只有 DeepSeek 家族会用 dense 层替换前若干 MoE 层)。
  4. 描述符缓存清理(python/sglang/kernels/ops/moe/fused_moe_triton_kernels.py):在模块级 _B_DESC_CACHE 旁新增 clear_b_tma_desc_cache(),benchmark_config 进入时立即调用。原因是 tuner 每个 config 都会重新分配 w1/w2,缓存里的旧 TensorDescriptor 只会钉住已死权重并让图捕获内存吃紧;服务化时权重与进程同生命周期,因此该清理在线上是 no-op,CUDA 服务路径未受影响。
  5. 配置数据与测试配套(configs/*.json、test_fused_moe_common_utils.py):新增 configs/triton_3_8_0/E=64,N=896,device_name=Intel(R)_Arc(TM)_Pro_B60_Graphics.json(上投影,17 个 batch size)与同名 _down.json(下投影,额外带 USE_TMA: true),按 shape 键控因此也适用于 DeepSeek-OCR v1。测试侧更新 Kimi-VL 断言以包含新增字段,并新增 test_get_model_config_reports_deepseek_ocr_layer_layout 覆盖 DeepSeek-OCR-2 的 12 层 / 1 dense 层布局。第 4 个 commit 额外移除了对已废弃 get_global_server_args 的使用。
文件 模块 状态 重要度
benchmark/kernels/fused_moe_triton/tuning_fused_moe_triton_sep.py 调优工具 modified 7.37
benchmark/kernels/fused_moe_triton/common_utils.py 模型配置 modified 5.59
test/registered/unit/layers/moe/test_fused_moe_common_utils.py 单元测试 modified 5.57
python/sglang/kernels/ops/moe/fused_moe_triton_kernels.py 内核缓存 modified 3.82
python/sglang/srt/layers/moe/moe_runner/triton_utils/configs/triton_3_8_0/E=64,N=896,device_name=Intel(R)_Arc(TM)_Pro_B60_Graphics.json 内核配置 added 5.46
python/sglang/srt/layers/moe/moe_runner/triton_utils/configs/triton_3_8_0/E=64,N=896,device_name=Intel(R)_Arc(TM)_Pro_B60_Graphics_down.json 内核配置 added 5.5
python/pyproject_xpu.toml 依赖声明 modified 3.37

关键符号

clear_b_tma_desc_cache test_get_model_config_reports_deepseek_ocr_layer_layout load_topk_ids benchmark_config KernelWrapper.cuda_graph_wrapper KernelWrapper.forward_cost BenchmarkWorker.__init__ get_model_config

关键源码片段

benchmark/kernels/fused_moe_triton/tuning_fused_moe_triton_sep.py core-logic

核心改造文件:把 CUDA 专用的调优器改为设备无关(sync/ 事件 / 图捕获 / 播种走 device_module),修正 Ray worker 设备绑定,并把 topk_ids 层布局从硬编码 61/3 改为从模型配置解析。

设备抽象与图捕获(重写后的相对完整实现):

# benchmark/kernels/fused_moe_triton/tuning_fused_moe_triton_sep.py
_is_hip = is_hip()
_is_xpu = is_xpu()
# 统一出口:CUDA/HIP 下是 torch.cuda,XPU 下是 torch.xpu
device_module = get_device_module()
​
​
class KernelWrapper:
    """把一个 MoE 调用序列包装成可重放的设备图,并在图外计时。"""
​
    def __init__(self, moe_inputs, use_cuda_graph=True, inner_iter=10, **kwargs):
        self.func = invoke_fused_moe_kernel
        self.use_cuda_graph = use_cuda_graph
        self.moe_inputs = moe_inputs
        self.inner_iter = inner_iter
        self.kwargs = kwargs
        self.graph = self.cuda_graph_wrapper() if use_cuda_graph else None
​
    def cuda_graph_wrapper(self):
        moe_input = self.moe_inputs[0]
        self.func(
            **self.kwargs,
            topk_ids=moe_input.topk_ids,
            sorted_token_ids=moe_input.sorted_token_ids,
            expert_ids=moe_input.expert_ids,
            num_tokens_post_padded=moe_input.num_tokens_post_padded,
        )
        # 原来是 torch.cuda.synchronize(),现在按设备派发
        device_module.synchronize()
​
        # 把 inner_iter 次调用捕获进一张可重放的图;XPU 需要用 XPUGraph 类
        graph_cls = torch.xpu.XPUGraph if _is_xpu else torch.cuda.CUDAGraph
        graph = graph_cls()
        with device_module.graph(graph):
            for k in range(self.inner_iter):
                moe_input = self.moe_inputs[k]
                self.func(
                    **self.kwargs,
                    topk_ids=moe_input.topk_ids,
                    sorted_token_ids=moe_input.sorted_token_ids,
                    expert_ids=moe_input.expert_ids,
                    num_tokens_post_padded=moe_input.num_tokens_post_padded,
                )
        device_module.synchronize()
​
        # 预热 5 次,把首次编译与首次执行排除在计时区间之外
        for _ in range(5):
            graph.replay()
        device_module.synchronize()
        return graph
​
    def forward_cost(self, try_cnt=2):
        time_cost = float("inf")
        for _ in range(try_cnt):
            # 事件同样走设备抽象,否则 XPU 上会创建 CUDA 事件
            start_event = device_module.Event(enable_timing=True)
            end_event = device_module.Event(enable_timing=True)
            start_event.record()
            if self.use_cuda_graph:
                self.graph.replay()
            else:
                for k in range(self.inner_iter):
                    moe_input = self.moe_inputs[k]
                    self.func(
                        **self.kwargs,
                        topk_ids=moe_input.topk_ids,
                        sorted_token_ids=moe_input.sorted_token_ids,
                        expert_ids=moe_input.expert_ids,
                        num_tokens_post_padded=moe_input.num_tokens_post_padded,
                    )
            end_event.record()
            device_module.synchronize()
            time_cost = min(time_cost, start_event.elapsed_time(end_event))
        return time_cost

层布局解析、描述符缓存清理与 Ray 设备绑定:

def load_topk_ids(topk_ids_dir, i: int):
    """按 MoE 层索引读取录制好的 topk_ids。
​
    层布局不再硬编码为 DeepSeek-V3 的 61 层 / 3 dense 层,而是从模型配置解析,
    因此 DeepSeek-OCR-2(12 层、首 1 层为 dense)才能取到正确文件。
    """
    model_config = get_model_config(
        get_model().model_path,
        tp_size=get_parallel().tp_size,
        ep_size=get_parallel().ep_size,
    )
    num_layers = model_config["num_layers"]
    dense_layers = model_config["dense_layers"]
    moe_layers = num_layers - dense_layers
    return torch.load(
        f"{topk_ids_dir}/topk_ids_layer{i % moe_layers + dense_layers}_idx{i // moe_layers}.pt"
    )
​
​
def benchmark_config(config: BenchmarkConfig, num_tokens: int, ...) -> float:
    ncu_enable = os.getenv("NCU_ENABLE", "0") == "1"
    if ncu_enable:
        num_iters = 1
    # 每个 config 都会重新分配 w1/w2,缓存里的旧 TensorDescriptor 只会钉住死权重,
    # 并让图捕获内存吃紧;服务化时权重与进程同生命周期,所以此清理在线上是 no-op。
    clear_b_tma_desc_cache()
    init_dtype = torch.float16 if use_fp8_w8a8 else dtype
    ...
​
​
class BenchmarkWorker:
    def __init__(self, seed: int, server_args: ServerArgs) -> None:
        torch.set_default_device(get_device())
        device_module.manual_seed_all(0)
        self.seed = seed
        # XPU 上 ray 的可见设备掩码生效太晚,必须显式使用 ray 分配的 device id,
        # 否则每个 actor 都落在 device 0 上,多卡调优结果全部作废。
        self.device_id = 0 if not ray.is_initialized() else int(ray.get_gpu_ids()[0])
        set_global_server_args_for_scheduler(server_args)
benchmark/kernels/fused_moe_triton/common_utils.py core-logic

为调优器提供层布局元数据:把 DeepseekOCRForCausalLM 纳入 DeepSeek MoE 分支,并在返回字典中新增 num_layers / dense_layers,直接决定 load_topk_ids 能否找到录制文件。

get_model_config 的关键分支与返回值(重写后的完整尾部逻辑):

# benchmark/kernels/fused_moe_triton/common_utils.py
    elif architecture in [
        "DeepseekV2ForCausalLM",
        "DeepseekV3ForCausalLM",
        "DeepseekV32ForCausalLM",
        "DeepseekV4ForCausalLM",
        # 新增:DeepSeek-OCR-2 与 DeepSeek 家族共用同一套 MoE 字段布局
        "DeepseekOCRForCausalLM",
        "Glm4MoeForCausalLM",
        "Glm4MoeLiteForCausalLM",
        "GlmMoeDsaForCausalLM",
        "KimiVLForConditionalGeneration",
        "MistralLarge3ForCausalLM",
    ]:
        E = (config.n_routed_experts // ep_size) + (
            0
            if disable_shared_experts_fusion
            or architecture
            not in [
                "DeepseekV3ForCausalLM",
                "DeepseekV32ForCausalLM",
                "Glm4MoeForCausalLM",
                "Glm4MoeLiteForCausalLM",
                "GlmMoeDsaForCausalLM",
                "MistralLarge3ForCausalLM",
            ]
            else 1
        )
        topk = config.num_experts_per_tok + (
            0 if disable_shared_experts_fusion or topk_ids_dir is None else 1
        )
        intermediate_size = config.moe_intermediate_size
​
    shard_intermediate_size = calculate_shard_intermediate_size(
        intermediate_size, tp_size, ep_size
    )
​
    # text_config 可能不带 torch_dtype,这里回退到 bf16
    torch_dtype = getattr(config, "torch_dtype", None) or torch.bfloat16
    num_layers = getattr(config, "num_hidden_layers", 0)
    # 只有 DeepSeek 家族会用 dense 层替换前若干层 MoE 层;
    # 取不到时默认 0,调用方需自行承担 moe_layers 为 0 的后果。
    dense_layers = getattr(config, "first_k_dense_replace", 0)
​
    return {
        "num_experts": E,
        "topk": topk,
        "hidden_size": hidden_size,
        "shard_intermediate_size": shard_intermediate_size,
        "dtype": torch_dtype,
        "block_shape": block_shape,
        "architecture": architecture,
        # 下面两个字段是本次新增,被 tuning_fused_moe_triton_sep.load_topk_ids 消费
        "num_layers": num_layers,
        "dense_layers": dense_layers,
    }
test/registered/unit/layers/moe/test_fused_moe_common_utils.py test-coverage

回归修复的落点:更新 Kimi-VL 断言以包含新增字段,并新增 DeepSeek-OCR 层布局覆盖,正是 review 中要求补的测试。

新增的 DeepSeek-OCR 层布局测试(完整用例):

# test/registered/unit/layers/moe/test_fused_moe_common_utils.py
def test_get_model_config_reports_deepseek_ocr_layer_layout():
    """load_topk_ids 按 MoE 层索引取录制文件,层数由 num_layers - dense_layers 推出,
    任一字段为 0 都会让 tuner 读到错误(或读不到)的录制数据。
​
    这个用例同时锁定了「层数字段来自 text_config 而非外层 config」这一事实。
    """
    common_utils = _load_common_utils()
    text_config = SimpleNamespace(
        hidden_size=1280,
        n_routed_experts=64,
        num_experts_per_tok=6,
        moe_intermediate_size=896,
        num_hidden_layers=12, # 12 层中首 1 层为 dense
        first_k_dense_replace=1,
        torch_dtype=torch.bfloat16,
    )
    model_config = SimpleNamespace(
        architectures=["DeepseekOCRForCausalLM"],
        text_config=text_config,
        get_text_config=lambda: text_config,
    )
​
    with patch.object(common_utils, "get_config", return_value=model_config):
        tuned_config = common_utils.get_model_config(
            "deepseek-ai/DeepSeek-OCR-2", tp_size=1, ep_size=1
        )
​
    assert tuned_config == {
        "num_experts": 64,
        "topk": 6,
        "hidden_size": 1280,
        "shard_intermediate_size": 1792,
        "dtype": torch.bfloat16,
        "block_shape": None,
        "architecture": "DeepseekOCRForCausalLM",
        "num_layers": 12,
        "dense_layers": 1,
    }

评论区精华

XPU 上 Triton 路线的投入策略与场景选择 设计

mingfeima 表示「our strategy is to keep triton path functional not necessarily performant」,认为 XPU 上 Triton 只是 nice to have,应保持最小投入,只适合 spec kernel 这类不重的计算;airMeng 认可调 config 的低成本高收益,但指出「DeepSeek-OCR is not quite critical」,建议转向 KV cache store 等 memory-bound kernel。作者引用 Intel Triton 官方性能指南(TensorDescriptor + autotune 是首要规则)回应,并强调不改 kernel 源码、无 XPU 专用分支、持续成本接近零。

结论:方案被接受(airMeng、mingfeima、polisettyvarma 均给出 approve),但「DeepSeek-OCR 不是关键场景」的意见未被解决,属于后续方向建议而非阻塞项。 · resolved_with_dissent

get_model_config 新增字段导致 base-a-test-cpu CI 回归 测试

mingfeima 以 CHANGES_REQUESTED 指出 common_utils.py 新增 num_layers / dense_layers 后未同步 test_fused_moe_common_utils.py,Kimi-VL 期望字典缺字段使共享 job 失败,要求「Need update the existing assertion and add coverage for the DeepSeek-OCR layer layout used by load_topk_ids」。作者在 3a37051cb1 修复并补测后,维护者仍强调「Please do self check next time. I am super busy right now and if you can self-check first to fix ci failures, it will save me a lot of effort.」

结论:回归已在 3a37051cb1 修复,Kimi-VL 断言与 DeepSeek-OCR 层布局用例同时补齐;流程层面留下「提交前先自检 CI」的要求。 · 已解决

调优器搜索空间曾被临时截断为前 4 个配置 正确性

gemini-code-assist[bot] 打 high priority:search_space = search_space[:4] 的临时截断必须在合并前移除,否则真实调优只会探索极小一部分搜索空间。

结论:最终提交的配置覆盖 18 个 batch size 的全量 sweep,截断未保留在最终版本;该文件也未出现在最终变更文件列表中。 · 已解决

XPU 上 Ray worker 的设备绑定与 dpctl 依赖 正确性

commit message 记录:XPU 上 Ray 的可见设备掩码生效太晚,若不显式应用 Ray 分配的设备 id,每个 actor 都会跑在 device 0 上,多卡调优结果全部作废;同时 Ray 在没有 dpctl 时不会注册 Intel GPU 资源。

结论:self.device_id 改为从 ray.get_gpu_ids() 推导,并在 pyproject_xpu.toml 新增 opt-in 的 ray extra(ray[default]>=2.55.1 与 dpctl),不并入 all 以免影响服务化安装。 · 已解决

B TensorDescriptor 缓存与图捕获的内存争用 性能

tuner 每个 config 都会重新分配 w1/w2,而 _B_DESC_CACHE 会为旧的权重对象保留描述符,既钉住已死权重又让图捕获内存吃紧;服务化时权重与进程同生命周期,不存在该问题。

结论:新增 clear_b_tma_desc_cache() 并在 benchmark_config 入口调用,服务路径行为不变。 · 已解决

风险与影响

  1. 层数解析的零值陷阱:load_topk_ids 依赖 model_config["num_layers"] - dense_layers,而 get_model_config 用 getattr(..., 0) 取值。若某模型配置缺 num_hidden_layers,moe_layers 为 0,i % moe_layers 会直接抛 ZeroDivisionError;新增测试的 docstring 已点明这一失败模式,但代码本身未做防御,属于调优工具对模型元数据的隐式契约。
  2. 配置按 device_name 字符串键控:新增配置文件名嵌入 Intel(R)_Arc(TM)_Pro_B60_Graphics,设备命名变化(驱动版本、B580 等兄弟型号、容器内改名)会导致查找未命中并回退 get_default_config(),最坏结果是性能退回基线而非报错;同时 USE_TMA: true 只出现在 _down.json,两侧配置字段并不对称。
  3. 精度证据强度有限:PR body 自述 olmOCR-bench 总量 85.6% 对 85.8%,但两臂并非同一测试集(old_scans 496 vs 472 个可评分样本),作者也承认差异量级约 0.4% 以内,只能读作「噪声内无回归」,不能作为严格零回归证明;bf16 归约顺序确实随 BLOCK_SIZE_M / BLOCK_SIZE_K 改变。
  4. 全局可变状态的导出:clear_b_tma_desc_cache 放在了 python/sglang/kernels/ops/moe/fused_moe_triton_kernels.py,是会被服务化代码导入的模块。当前唯一调用点在 benchmark 内,但接口无访问约束,若被误用到服务路径会清空 B 描述符缓存并带来重建开销。
  5. CI 与可复现性:本 PR 曾打断共享 base-a-test-cpu job,修复虽已合入,但 PR body 中的最新 PR Test(Base)与 Extra 仍显示失败/未通过标记,落地前需要确认是否还有残余失败;此外 --configs 模式一次只对一个投影生效,复现一组数据需要三次调用人工拼装(PR body 已给出脚本),测量结果依赖手工步骤,误差控制较脆弱。
  6. 安装路径未文档化:tuner 需要 ray 与 dpctl,但只作为 opt-in extra 存在且不并入 all,使用者在 XPU 上跑 tuner 时需自行安装 sglang[ray],PR 未附使用说明。

对用户:在 Intel Arc Pro B60 上以 --moe-runner-backend triton 跑 DeepSeek-OCR-2 的用户可拿到约 +13% 输出吞吐、约 +12.8%~13.6% 的 TPOT/ITL 改善(eager 与 XPU graph 两种模式结论一致),代价是新增两份 device 级配置;不指定 triton 后端时 moe_runner_backend 会解析为 auto,配置查找不会被触达,收益为零。对系统:CUDA/HIP 服务化路径完全未改,clear_b_tma_desc_cache 在线上为 no-op;配置目录新增按设备名键控的文件,对其他设备无影响。对团队:调优器从 CUDA 专用变为设备无关工具,后续 AMD/NPU 调优可以直接复用同一套 device 抽象与 ray 绑定逻辑;同时暴露出「改了 benchmark 工具的公共返回契约却没有同步测试」的协作问题,维护者明确要求提交前自检 CI。

调优工具隐式依赖模型元数据 配置按 device_name 字符串键控 精度对比非同一测试集 曾打断 base-a-test-cpu CI 全局缓存清理接口无访问约束 ray extra 安装路径未文档化

关联 Issue

#7306 Host-side tensor descriptors drop the 2D-block-I/O fast path on XPU (BMG/CRI)
#7691 [SGLang] [MoE] TensorDescriptor shape specialization causes one JIT compile per batch size

完整报告

参与讨论