Skip to content

Add GPU backend for the searcher-findee / cophenetic pipeline - #3

Open
hutaobo wants to merge 5 commits into
mainfrom
gpu-cophenetic-backend
Open

Add GPU backend for the searcher-findee / cophenetic pipeline#3
hutaobo wants to merge 5 commits into
mainfrom
gpu-cophenetic-backend

Conversation

@hutaobo

@hutaobo hutaobo commented May 31, 2026

Copy link
Copy Markdown
Owner

Summary

The whole Cell-GPS cophenetic / StructureMap pipeline (the README's main architecture) rests on one heavy computation — the directed inter-group average nearest-neighbour distance ("searcher→findee"), one sklearn.neighbors.NearestNeighbors 1-NN model per cell type. For large cell counts that O(N²) step dominates; everything downstream (linkage/cophenet on the small k×k cluster matrix) is negligible.

This PR adds a PyTorch GPU implementation of that kernel and threads a backend switch through the main README entry points, so the documented pipeline can run end-to-end on a GPU.

What changed

  • New src/sfplot/analysis/searcher_findee_score_gpu.py — batched torch.cdist GPU kernel + a shared nearest_cluster_distance_columns_gpu primitive.
  • backend switch ("cpu" default, "gpu"/"cuda", "auto") added to:
    • compute_searcher_findee_distance_matrix_from_df
    • compute_cophenetic_distances_from_df
    • compute_cophenetic_distances_from_adata
  • Public export compute_searcher_findee_distance_matrix_from_df_gpu (via cellgps and sfplot).
  • Tests tests/test_searcher_findee_gpu_equivalence.py — run the GPU code path on device="cpu" so they execute in CI without a GPU.
  • Benchmark examples/gpu_searcher_findee_benchmark.py — verify equivalence + measure speedup on real CUDA hardware (warm-up + cuda.synchronize).
  • README: short "GPU acceleration" section.

Equivalence (the contract)

The GPU path is numerically equivalent to the scikit-learn path. With the default gpu_dtype="float64" and the exact Euclidean formula (compute_mode="donot_use_mm_for_euclid_dist"), it reproduces the CPU result exactly:

searcher_findee  2D Δ=0.00e+00  3D Δ=0.00e+00  auto Δ=0.00e+00
cophenetic_from_df    row Δ=0.00e+00  col Δ=0.00e+00
cophenetic_from_adata row Δ=0.00e+00  col Δ=0.00e+00
WORST float64 Δ end-to-end = 0.00e+00  -> GPU == CPU across pipeline

verified across 2D / 3D / integer-coordinate inputs and k = 5..20. gpu_dtype="float32" is the fast mode at ~1e-5 relative deviation.

Compatibility

  • backend="cpu" remains the default — existing behaviour is unchanged.
  • GPU requires torch (already declared as the Cell-GPS[gpu] extra). The GPU module is imported lazily, only when a GPU backend is requested.
  • Full suite: 27 passed (8 new + 19 existing).

🤖 Generated with Claude Code

Summary by Sourcery

添加一个基于 GPU 的 PyTorch 实现,用于 searcher–findee 最近邻核函数,并在 cophenetic/StructureMap 流水线中贯穿后端切换开关,从而在保持 CPU 默认行为的同时支持在 GPU 上运行。

New Features:

  • 引入基于 PyTorch 的 GPU 实现,用于 searcher–findee 最近邻距离核函数,并将其暴露为公共入口。
  • 为 cophenetic 和 searcher–findee 距离计算 API 增加后端选择器(cpu/gpu/auto),包括对 GPU 特定选项(如 device、dtype 和内存限制)的支持。
  • 提供基准测试脚本,用于验证数值等价性并衡量 searcher–findee 流水线在 CPU 与 GPU 上的性能差异。

Enhancements:

  • 将分组操作更新为使用 observed=False,以使 groupby 行为与现有语义保持一致。
  • 在 README 中记录 GPU 加速的使用方式与配置,包括关于精度与性能权衡的指导。

Tests:

  • 添加 GPU/CPU 等价性测试,在 CPU 或 CUDA 上运行 GPU 代码路径,并断言 float64 结果与现有 scikit-learn 实现完全一致。
  • 验证公共 GPU API 通过顶层 cellgps/sfplot 命名空间对外导出。
Original summary in English

Summary by Sourcery

Add a GPU-backed PyTorch implementation of the searcher–findee nearest-neighbor kernel and thread a backend switch through the cophenetic/StructureMap pipeline so it can run on GPU while preserving CPU-default behavior.

New Features:

  • Introduce a PyTorch-based GPU implementation of the searcher–findee nearest-neighbor distance kernel and expose it as a public entry point.
  • Add a backend selector (cpu/gpu/auto) to cophenetic and searcher–findee distance computation APIs, including support for GPU-specific options like device, dtype, and memory limits.
  • Provide a benchmark script to validate numerical equivalence and measure CPU vs GPU performance for the searcher–findee pipeline.

Enhancements:

  • Update grouping operations to use observed=False to align groupby behavior with existing semantics.
  • Document GPU acceleration usage and configuration in the README, including guidance on precision vs performance trade-offs.

Tests:

  • Add GPU/CPU equivalence tests that run the GPU code path (on CPU or CUDA) and assert float64 results match the existing scikit-learn implementation exactly.
  • Verify the public GPU API is exported via the top-level cellgps/sfplot namespaces.

The cophenetic / StructureMap pipeline is built on one heavy computation: the
directed inter-group average nearest-neighbour distance ("searcher-findee").
This adds a PyTorch GPU implementation of that kernel and threads a `backend`
switch ("cpu" default, "gpu"/"cuda", "auto") through the main README entry
points: compute_searcher_findee_distance_matrix_from_df,
compute_cophenetic_distances_from_df, and compute_cophenetic_distances_from_adata.

The GPU path is numerically equivalent to the scikit-learn path: with the
default gpu_dtype="float64" it reproduces the CPU result exactly (max abs diff
= 0.0 across 2D / 3D / integer-coordinate cases, k = 5..20); gpu_dtype="float32"
is the fast mode at ~1e-5 relative deviation. backend="cpu" stays the default,
so existing behaviour is unchanged. Only the O(N^2) nearest-neighbour step moves
to the GPU; the small k x k linkage/cophenet tail stays on CPU.

New:
- src/sfplot/analysis/searcher_findee_score_gpu.py: GPU kernel + shared NN primitive
- tests/test_searcher_findee_gpu_equivalence.py: CPU/GPU equivalence (runs on CPU in CI)
- examples/gpu_searcher_findee_benchmark.py: verify + benchmark on real CUDA hardware

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 31, 2026 19:54
@sourcery-ai

sourcery-ai Bot commented May 31, 2026

Copy link
Copy Markdown

审阅者指南

为 searcher→findee 最近邻内核新增一个基于 PyTorch 的 GPU 后端,并将后端切换选项贯穿接线到 cophenetic 流水线中,同时确保数值等价并保持现有的 CPU 行为不变。

cophenetic 流水线中 CPU 与 GPU 后端选择的流程图

flowchart LR
    User["User code
compute_cophenetic_distances_from_df / _from_adata"]
    Backend["_resolve_backend(backend)"]
    CPUPath["CPU path
sklearn NearestNeighbors
per cluster"]
    GPUPath["GPU path
compute_searcher_findee_distance_matrix_from_df_gpu"]
    Primitive["nearest_cluster_distance_columns_gpu
(torch.cdist)"]
    DistMat["searcher_findee distance matrix"]
    Coph["cophenetic distances
(row, col)"]

    User --> Backend
    Backend -->|cpu| CPUPath
    Backend -->|gpu or auto and cuda| GPUPath

    CPUPath --> DistMat
    GPUPath --> Primitive --> DistMat

    DistMat --> Coph
Loading

文件级变更

变更 详情 文件
引入基于 GPU(PyTorch)的 searcher→findee 最近邻内核实现及共享原语,并支持内存感知分批和 dtype 处理。
  • 新增 searcher_findee_score_gpu 模块,使用分批的 torch.cdist 和精确欧式距离模式实现 GPU 内核。
  • 实现辅助函数,用于解析 torch dtype、在给定可配置内存预算的前提下通过分块计算到参考集合的最近距离,并为所有簇生成每个细胞的最近距离列。
  • 提供 compute_searcher_findee_distance_matrix_from_df 的 GPU 变体,在验证逻辑、类别处理、分组以及删除全 NaN 列的语义上与 CPU 版本保持一致。
src/sfplot/analysis/searcher_findee_score_gpu.py
在 cophenetic/searcher-findee 公共 API 中贯穿可配置的后端(cpu/gpu/auto)及 GPU 选项,同时保持默认 CPU 行为。
  • 新增 _resolve_backend 辅助函数,用于规范化后端字符串,支持 'cpu''gpu'/'cuda''auto',当为 'auto' 时会检测 torch.cuda 的可用性,并在未知值时抛出异常。
  • 扩展 compute_cophenetic_distances_from_adata,使其接受 backenddevicegpu_dtypegpu_max_memory_gb,在后端解析为 GPU 时派发到用于最近簇距离的 GPU 原语,否则使用现有的 scikit-learn 路径。
  • 扩展 compute_searcher_findee_distance_matrix_from_dfcompute_cophenetic_distances_from_df,新增 backend/device/gpu_dtype/gpu_max_memory_gb 参数,并在请求 GPU 时路由到 GPU 实现,同时保持现有 CPU 代码路径不变。
  • groupby 调用调整为使用 observed=False,以在簇标签为分类变量时保持行为一致。
src/sfplot/analysis/searcher_findee_score.py
通过公共的 sfplot/cellgps API 对外导出 GPU 内核,并在 README 中记录 GPU 加速能力。
  • sfplot.__init__sfplot.analysis.__init__ 中注册 compute_searcher_findee_distance_matrix_from_df_gpu,以便其以惰性导入方式通过包命名空间对外暴露。
  • 在 README 中新增章节,说明 GPU 加速、后端切换用法、数值等价保证及安装要求,并将该 GPU 内核列为一个有用的公共入口点。
src/sfplot/__init__.py
src/sfplot/analysis/__init__.py
README.md
新增测试以确保 GPU/CPU 数值等价和后端行为,并提供用于真实 GPU 校验的基准测试脚本。
  • 创建测试套件,构造合成的 DataFrame/类 AnnData 对象,分别运行 CPU 与 GPU 代码路径(其中 GPU 被强制为 device='cpu'),并在 searcher-findee 矩阵和 cophenetic 输出上断言 float64 等价及 float32 近似一致。
  • 新增测试,用于覆盖 auto 后端解析、未知后端时报错、以及通过 cellgps 包对公共导出进行存在性检查。
  • 引入基准脚本,对不同规模下 CPU 与 GPU 的 searcher-findee 内核进行计时,测量 float64/float32 的最大绝对误差,并可选地将结果写入 JSON,同时包含正确的 CUDA 同步与预热逻辑。
tests/test_searcher_findee_gpu_equivalence.py
examples/gpu_searcher_findee_benchmark.py

提示和命令

与 Sourcery 交互

  • 触发新的审查: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 在审查评论下回复并请求 Sourcery 从该评论创建 issue。你也可以直接回复 @sourcery-ai issue 来从该评论创建 issue。
  • 生成 pull request 标题: 在 pull request 标题中任意位置写上 @sourcery-ai,即可在任意时间生成一个标题。你也可以在 pull request 中评论 @sourcery-ai title 以在任意时间(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文任意位置写上 @sourcery-ai summary,即可在你希望的位置生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 以在任意时间(重新)生成摘要。
  • 生成审阅者指南: 在 pull request 中评论 @sourcery-ai guide,即可在任意时间(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 忽略所有 Sourcery 审查: 在 pull request 中评论 @sourcery-ai dismiss,即可忽略所有现有的 Sourcery 审查。特别适用于你想从一次全新的审查开始时——别忘了再评论一次 @sourcery-ai review 来触发新的审查!

自定义你的体验

访问你的 dashboard 以:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审阅者指南等。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查指令。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

Adds a PyTorch-based GPU backend for the searcher→findee nearest-neighbor kernel and wires a backend switch through the cophenetic pipeline, while ensuring numerical equivalence and preserving existing CPU behavior.

Flow diagram for CPU vs GPU backend selection in cophenetic pipeline

flowchart LR
    User["User code
compute_cophenetic_distances_from_df / _from_adata"]
    Backend["_resolve_backend(backend)"]
    CPUPath["CPU path
sklearn NearestNeighbors
per cluster"]
    GPUPath["GPU path
compute_searcher_findee_distance_matrix_from_df_gpu"]
    Primitive["nearest_cluster_distance_columns_gpu
(torch.cdist)"]
    DistMat["searcher_findee distance matrix"]
    Coph["cophenetic distances
(row, col)"]

    User --> Backend
    Backend -->|cpu| CPUPath
    Backend -->|gpu or auto and cuda| GPUPath

    CPUPath --> DistMat
    GPUPath --> Primitive --> DistMat

    DistMat --> Coph
Loading

File-Level Changes

Change Details Files
Introduce a GPU (PyTorch) implementation of the searcher→findee nearest-neighbor kernel and shared primitives, with memory-aware batching and dtype handling.
  • Add new searcher_findee_score_gpu module implementing GPU kernel using batched torch.cdist and exact Euclidean distance mode.
  • Implement helper functions to resolve torch dtypes, compute nearest distances to a reference set with chunking based on a configurable memory budget, and generate per-cell nearest-distance columns for all clusters.
  • Provide a GPU variant of compute_searcher_findee_distance_matrix_from_df that mirrors CPU validation, category handling, grouping, and NaN-column dropping semantics.
src/sfplot/analysis/searcher_findee_score_gpu.py
Thread a configurable backend (cpu/gpu/auto) and GPU options through the cophenetic/searcher-findee public APIs while preserving default CPU behavior.
  • Add _resolve_backend helper that normalizes backend strings, supports 'cpu', 'gpu'/'cuda', and 'auto' with torch.cuda availability detection, and raises on unknown values.
  • Extend compute_cophenetic_distances_from_adata to accept backend, device, gpu_dtype, and gpu_max_memory_gb, dispatching to the GPU primitive for nearest-cluster distances when backend resolves to GPU and otherwise using the existing scikit-learn path.
  • Extend compute_searcher_findee_distance_matrix_from_df and compute_cophenetic_distances_from_df with backend/device/gpu_dtype/gpu_max_memory_gb parameters and route to the GPU implementation when requested, while keeping the existing CPU code path intact.
  • Adjust groupby calls to use observed=False to preserve behavior with categorical cluster labels.
src/sfplot/analysis/searcher_findee_score.py
Export the GPU kernel via the public sfplot/cellgps API surface and document GPU acceleration in the README.
  • Register compute_searcher_findee_distance_matrix_from_df_gpu in sfplot.init and sfplot.analysis.init so it is lazily imported and exposed via the package namespace.
  • Add README section explaining GPU acceleration, backend switch usage, numerical equivalence guarantees, and installation requirements, and list the GPU kernel as a useful public entry point.
src/sfplot/__init__.py
src/sfplot/analysis/__init__.py
README.md
Add tests to ensure GPU/CPU numerical equivalence and backend behavior, and provide a benchmarking script for real-GPU validation.
  • Create test suite that constructs synthetic DataFrames/AnnData-like objects, runs CPU vs GPU code paths (with GPU forced to device='cpu'), and asserts float64 equivalence and float32 closeness across searcher-findee matrices and cophenetic outputs.
  • Add tests for auto backend resolution, error handling for unknown backends, and public export presence via the cellgps package.
  • Introduce a benchmark script that times CPU vs GPU searcher-findee kernels over varying sizes, measures max absolute differences for float64/float32, and optionally writes results to JSON, including proper CUDA synchronization and warm-up.
tests/test_searcher_findee_gpu_equivalence.py
examples/gpu_searcher_findee_benchmark.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional PyTorch-based backend for the core “searcher→findee” nearest-neighbor kernel and threads a backend selector through the public cophenetic/StructureMap entry points so the documented pipeline can run on GPU while preserving the existing CPU-default behavior.

Changes:

  • Introduces src/sfplot/analysis/searcher_findee_score_gpu.py implementing a batched torch.cdist nearest-neighbor primitive and a GPU version of the distance-matrix API.
  • Adds backend/GPU control parameters to the main computation entry points and wires in "cpu" | "gpu" | "auto" backend resolution.
  • Adds GPU/CPU equivalence tests (running the GPU code path on device="cpu"), a benchmark example script, and README documentation for GPU usage.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/sfplot/analysis/searcher_findee_score.py Adds backend selection and routes computation to the GPU implementation when requested.
src/sfplot/analysis/searcher_findee_score_gpu.py New PyTorch implementation of the nearest-neighbor kernel and GPU distance-matrix entry point.
src/sfplot/analysis/__init__.py Exposes the new GPU entry point via lazy exports.
src/sfplot/__init__.py Exposes the new GPU entry point at the legacy top-level namespace.
tests/test_searcher_findee_gpu_equivalence.py New equivalence tests for CPU vs GPU backend behavior.
examples/gpu_searcher_findee_benchmark.py New benchmark/equivalence script for real CUDA hardware.
README.md Documents the new backend switch and GPU dtype tradeoffs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_searcher_findee_gpu_equivalence.py Outdated
Comment thread src/sfplot/analysis/searcher_findee_score.py
Comment thread examples/gpu_searcher_findee_benchmark.py Outdated
Comment thread tests/test_searcher_findee_gpu_equivalence.py Outdated
Comment thread src/sfplot/analysis/searcher_findee_score.py
Comment thread examples/gpu_searcher_findee_benchmark.py Outdated

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题

给 AI 代理的提示
请根据这次代码审查中的评论进行修改:

## 单独评论

### 评论 1
<location path="examples/gpu_searcher_findee_benchmark.py" line_range="79" />
<code_context>
+    return (time.perf_counter() - t0) / reps
+
+
+def main() -> int:
+    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+    p.add_argument("--max-n", type=int, default=100_000, help="largest cell count to benchmark")
</code_context>
<issue_to_address>
**issue (complexity):** 建议通过将等价性检查/基准测试逻辑以及通用辅助函数抽取到更专注的函数,或共享的工具模块中,来重构体积较大的 main() 函数,从而让脚本更易于理解和维护。

你可以在不改变任何行为的前提下,通过把 `main()` 中的逻辑拆分成更聚焦的辅助函数,并将“工具”部分隔离出来,来降低这个示例的认知负担。这样既能保持示例的可读性,又能保留所有 CLI 选项和 JSON 输出。

**1. 将等价性检查和基准测试提取为单独函数**

这会让 `main()` 的执行流程更容易跟踪,并避免变成一个 140 行的大块函数:

```python
EQUIV_N = 20_000
BENCHMARK_SIZES = (1_000, 2_000, 5_000, 10_000, 20_000, 50_000,
                   100_000, 200_000, 500_000, 1_000_000)

def run_equivalence(args, cpu_kw, device: str) -> tuple[float, float, bool]:
    n = min(EQUIV_N, args.max_n)
    df = make_df(n, args.k, args.dims, seed=12345)
    cpu = sf(df, **cpu_kw)
    d64 = max_abs_diff(cpu, sf(df, z_col=cpu_kw["z_col"], backend="gpu",
                               device=device, gpu_dtype="float64",
                               gpu_max_memory_gb=args.max_memory_gb))
    d32 = max_abs_diff(cpu, sf(df, z_col=cpu_kw["z_col"], backend="gpu",
                               device=device, gpu_dtype="float32",
                               gpu_max_memory_gb=args.max_memory_gb))
    ok = d64 < 1e-9
    print(f"[1] EQUIVALENCE (n={n})")
    print(f"    float64 max|delta| = {d64:.3e}   -> {'OK (GPU == CPU)' if ok else 'MISMATCH'}")
    print(f"    float32 max|delta| = {d32:.3e}   (fast mode)\n")
    return d64, d32, ok

def run_benchmark(args, cpu_kw, gpu_kw) -> list[dict]:
    sizes = [n for n in BENCHMARK_SIZES if n <= args.max_n]
    print("[2] BENCHMARK (seconds/call; speedup = CPU/GPU)")
    print(f"    {'N':>9} {'CPU(s)':>10} {'GPU(s)':>10} {'speedup':>9}")
    rows: list[dict] = []
    for n in sizes:
        df = make_df(n, args.k, args.dims, seed=n)
        reps = max(1, int(200_000 // n))
        t_cpu = time_call(lambda: sf(df, **cpu_kw), reps, "cpu")
        t_gpu = time_call(lambda: sf(df, **gpu_kw), reps, args.device)
        speedup = t_cpu / t_gpu if t_gpu > 0 else float("nan")
        print(f"    {n:>9} {t_cpu:>10.4f} {t_gpu:>10.4f} {speedup:>8.2f}x")
        rows.append(dict(n=n, cpu_s=t_cpu, gpu_s=t_gpu, speedup=speedup, reps=reps))
    return rows
```

然后 `main()` 主要就变成了一个调度函数:

```python
def main() -> int:
    args = parse_args()  # 一个小的辅助函数,只负责构建并返回 argparse.Namespace

    z_col = "z" if args.dims == 3 else None
    cuda_ok = torch.cuda.is_available()
    print(f"torch {torch.__version__}  device={args.device}  cuda_available={cuda_ok}")
    if args.device.startswith("cuda") and not cuda_ok:
        print("WARNING: CUDA not available; GPU timings will fall back to CPU and be meaningless.")
    print(f"params: k={args.k} dims={args.dims} gpu_dtype={args.dtype} max_n={args.max_n}\n")

    cpu_kw = dict(z_col=z_col, backend="cpu")
    gpu_kw = dict(z_col=z_col, backend="gpu", device=args.device,
                  gpu_dtype=args.dtype, gpu_max_memory_gb=args.max_memory_gb)

    d64, d32, ok = run_equivalence(args, cpu_kw, args.device)
    rows = run_benchmark(args, cpu_kw, gpu_kw)

    if args.json:
        write_json(args, cuda_ok, d64, d32, ok, rows)  # 小的辅助函数

    return 0 if ok else 1
```

**2. 将通用辅助函数移动到共享工具模块(可选但成本较低)**

`max_abs_diff``time_call` 足够通用,可以放到一个内部的小模块中(例如 `examples/_utils.py``cellgps/_bench_utils.py`),以便在其他示例/测试中复用:

```python
# examples/_utils.py
import numpy as np
import time
import torch

def max_abs_diff(a, b) -> float:
    a = a.reindex(index=b.index, columns=b.columns)
    av, bv = a.values.astype(float), b.values.astype(float)
    both = ~(np.isnan(av) | np.isnan(bv))
    return float(np.abs(av[both] - bv[both]).max()) if both.any() else 0.0

def time_call(fn, reps: int, device: str) -> float:
    fn()
    if device.startswith("cuda"):
        torch.cuda.synchronize()
    t0 = time.perf_counter()
    for _ in range(reps):
        fn()
    if device.startswith("cuda"):
        torch.cuda.synchronize()
    return (time.perf_counter() - t0) / reps
```

然后在这个示例中:

```python
from examples._utils import max_abs_diff, time_call
```

这样可以让示例专注于“如何使用 GPU 后端”,同时保留当前的所有功能(CLI 接口、JSON、基准测试、等价性检查)。
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得我们的审查有帮助,请考虑分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进之后的审查。
Original comment in English

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="examples/gpu_searcher_findee_benchmark.py" line_range="79" />
<code_context>
+    return (time.perf_counter() - t0) / reps
+
+
+def main() -> int:
+    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+    p.add_argument("--max-n", type=int, default=100_000, help="largest cell count to benchmark")
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the large main() function by extracting equivalence/benchmark logic and generic helpers into focused functions or a shared utility module to make the script easier to understand and maintain.

You can reduce the cognitive load of this example without changing any behavior by splitting the logic in `main()` into focused helpers and isolating the “utility” pieces. That keeps the example readable while preserving all CLI options and JSON output.

**1. Extract equivalence + benchmark into separate functions**

This makes the flow of `main()` much easier to follow and avoids the 140-line monolith:

```python
EQUIV_N = 20_000
BENCHMARK_SIZES = (1_000, 2_000, 5_000, 10_000, 20_000, 50_000,
                   100_000, 200_000, 500_000, 1_000_000)

def run_equivalence(args, cpu_kw, device: str) -> tuple[float, float, bool]:
    n = min(EQUIV_N, args.max_n)
    df = make_df(n, args.k, args.dims, seed=12345)
    cpu = sf(df, **cpu_kw)
    d64 = max_abs_diff(cpu, sf(df, z_col=cpu_kw["z_col"], backend="gpu",
                               device=device, gpu_dtype="float64",
                               gpu_max_memory_gb=args.max_memory_gb))
    d32 = max_abs_diff(cpu, sf(df, z_col=cpu_kw["z_col"], backend="gpu",
                               device=device, gpu_dtype="float32",
                               gpu_max_memory_gb=args.max_memory_gb))
    ok = d64 < 1e-9
    print(f"[1] EQUIVALENCE (n={n})")
    print(f"    float64 max|delta| = {d64:.3e}   -> {'OK (GPU == CPU)' if ok else 'MISMATCH'}")
    print(f"    float32 max|delta| = {d32:.3e}   (fast mode)\n")
    return d64, d32, ok

def run_benchmark(args, cpu_kw, gpu_kw) -> list[dict]:
    sizes = [n for n in BENCHMARK_SIZES if n <= args.max_n]
    print("[2] BENCHMARK (seconds/call; speedup = CPU/GPU)")
    print(f"    {'N':>9} {'CPU(s)':>10} {'GPU(s)':>10} {'speedup':>9}")
    rows: list[dict] = []
    for n in sizes:
        df = make_df(n, args.k, args.dims, seed=n)
        reps = max(1, int(200_000 // n))
        t_cpu = time_call(lambda: sf(df, **cpu_kw), reps, "cpu")
        t_gpu = time_call(lambda: sf(df, **gpu_kw), reps, args.device)
        speedup = t_cpu / t_gpu if t_gpu > 0 else float("nan")
        print(f"    {n:>9} {t_cpu:>10.4f} {t_gpu:>10.4f} {speedup:>8.2f}x")
        rows.append(dict(n=n, cpu_s=t_cpu, gpu_s=t_gpu, speedup=speedup, reps=reps))
    return rows
```

Then `main()` becomes mostly orchestration:

```python
def main() -> int:
    args = parse_args()  # small helper that just builds/returns argparse.Namespace

    z_col = "z" if args.dims == 3 else None
    cuda_ok = torch.cuda.is_available()
    print(f"torch {torch.__version__}  device={args.device}  cuda_available={cuda_ok}")
    if args.device.startswith("cuda") and not cuda_ok:
        print("WARNING: CUDA not available; GPU timings will fall back to CPU and be meaningless.")
    print(f"params: k={args.k} dims={args.dims} gpu_dtype={args.dtype} max_n={args.max_n}\n")

    cpu_kw = dict(z_col=z_col, backend="cpu")
    gpu_kw = dict(z_col=z_col, backend="gpu", device=args.device,
                  gpu_dtype=args.dtype, gpu_max_memory_gb=args.max_memory_gb)

    d64, d32, ok = run_equivalence(args, cpu_kw, args.device)
    rows = run_benchmark(args, cpu_kw, gpu_kw)

    if args.json:
        write_json(args, cuda_ok, d64, d32, ok, rows)  # small helper

    return 0 if ok else 1
```

**2. Move generic helpers into a shared utility (optional but low-cost)**

`max_abs_diff` and `time_call` are generic enough to live in a small internal module (e.g. `examples/_utils.py` or `cellgps/_bench_utils.py`) and be reused by other examples/tests:

```python
# examples/_utils.py
import numpy as np
import time
import torch

def max_abs_diff(a, b) -> float:
    a = a.reindex(index=b.index, columns=b.columns)
    av, bv = a.values.astype(float), b.values.astype(float)
    both = ~(np.isnan(av) | np.isnan(bv))
    return float(np.abs(av[both] - bv[both]).max()) if both.any() else 0.0

def time_call(fn, reps: int, device: str) -> float:
    fn()
    if device.startswith("cuda"):
        torch.cuda.synchronize()
    t0 = time.perf_counter()
    for _ in range(reps):
        fn()
    if device.startswith("cuda"):
        torch.cuda.synchronize()
    return (time.perf_counter() - t0) / reps
```

Then in the example:

```python
from examples._utils import max_abs_diff, time_call
```

This keeps the example focused on “how to use the GPU backend” while preserving all current functionality (CLI surface, JSON, benchmarking, equivalence check).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread examples/gpu_searcher_findee_benchmark.py Outdated
hutaobo and others added 4 commits June 1, 2026 10:52
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants