<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><title>Xa Inkwell</title><link>https://benyoeW.github.io/Inkwell</link><description>daily learning &amp; working</description><copyright>Xa Inkwell</copyright><docs>http://www.rssboard.org/rss-specification</docs><generator>python-feedgen</generator><image><url>https://github.githubassets.com/favicons/favicon.svg</url><title>avatar</title><link>https://benyoeW.github.io/Inkwell</link></image><lastBuildDate>Fri, 03 Apr 2026 03:21:43 +0000</lastBuildDate><managingEditor>Xa Inkwell</managingEditor><ttl>60</ttl><webMaster>Xa Inkwell</webMaster><item><title>【编程基础】后端接口请求实例（以profilling agent为例）</title><link>https://benyoeW.github.io/Inkwell/post/%E3%80%90-bian-cheng-ji-chu-%E3%80%91-hou-duan-jie-kou-qing-qiu-shi-li-%EF%BC%88-yi-profilling%20agent-wei-li-%EF%BC%89.html</link><description># curl测试接口
```
curl -X POST http://127.0.0.1:3000/api/v1/gpu/red-black/tasks/query-wiki-link \
  -H 'Content-Type: application/json' \
  -d '{
    'stat_cycle_type': 'week',
    'stat_cycle_value': '2026.2.1-2026.2.8',
    'rank_type': 'red',
    'biz_category': 'AMU'
  }'
```
# 一、整体设计模块（四层架构）
```
┌─────────────────────────────────────────────────────────┐
│  1. 路由层 (Router)         — 接收 HTTP 请求，分发给 Service │
│     gpu_red_black_task_router.py                        │
├─────────────────────────────────────────────────────────┤
│  2. 数据模型层 (Models)      — 定义请求体/响应体的结构       │
│     gpu_red_black_task_models.py                        │
├─────────────────────────────────────────────────────────┤
│  3. 服务层 (Service)        — 核心业务逻辑 + 数据库操作      │
│     gpu_red_black_task_service.py                       │
├─────────────────────────────────────────────────────────┤
│  4. ORM 模型 (数据库表映射)   — Python 类 ↔ 数据库表         │
│     也在 gpu_red_black_task_service.py 中定义             │
└─────────────────────────────────────────────────────────┘
再加上一个贯穿各层的关键机制：

  5. 依赖注入 (DI Injector)   — 管理数据库连接的生命周期
     SQLGpuRedBlackTaskServiceInjector
还有一个 路由注册入口：

  6. v1_router.py             — 将各模块的 router 注册到主应用

```

# 二、各模块详解





。</description><guid isPermaLink="true">https://benyoeW.github.io/Inkwell/post/%E3%80%90-bian-cheng-ji-chu-%E3%80%91-hou-duan-jie-kou-qing-qiu-shi-li-%EF%BC%88-yi-profilling%20agent-wei-li-%EF%BC%89.html</guid><pubDate>Fri, 03 Apr 2026 03:21:12 +0000</pubDate></item><item><title>eagle123</title><link>https://benyoeW.github.io/Inkwell/post/eagle123.html</link><description>EAGLE-1 的树结构是这样工作的：
离线阶段：在训练/评估阶段，通过统计大量样本，分析'第 i 个位置、第 j 个候选'的平均接受率，然后用动态规划或贪心搜索，在给定的计算 budget（树的总节点数）下，找到期望接受 token 数最多的树形状。</description><guid isPermaLink="true">https://benyoeW.github.io/Inkwell/post/eagle123.html</guid><pubDate>Sat, 21 Mar 2026 06:09:42 +0000</pubDate></item><item><title>MOE</title><link>https://benyoeW.github.io/Inkwell/post/MOE.html</link><description>以 DeepSeek-V3/V2 的具体参数为基准（N=256 个专家，Top-K=8，每个 expert 的 intermediate 维度为 2048，shared experts 另计）

**第一步：Gate 打分（对每个token进行路由决策）**
每个 token 的 hidden state 先经过一个轻量线性层，得到对所有专家的亲和度分数。</description><guid isPermaLink="true">https://benyoeW.github.io/Inkwell/post/MOE.html</guid><pubDate>Fri, 20 Mar 2026 03:47:47 +0000</pubDate></item><item><title>【vllm】DeepSeekMTP、eagle</title><link>https://benyoeW.github.io/Inkwell/post/%E3%80%90vllm%E3%80%91DeepSeekMTP%E3%80%81eagle.html</link><description># 全局整体调用逻辑：
```
run_busy_loop()
  ├─ _process_input_queue()   ← 从 input_queue 读取新请求/abort     # 将请求传输到引擎侧，然后执行下面的函数
  └─ _process_engine_step() 
       ├─ step_fn()            ← 即 step() 或 step_with_batch_queue()
       │    ├─ scheduler.schedule()                    ← ① 调度，决定本轮送哪些请求
       │    ├─ executor.execute_model(sched_output)   ← ② 触发 GPU 前向
       │    ├─ scheduler.get_grammar_bitmask()         ← ③ 结构化输出的语法掩码
       │    ├─ executor.sample_tokens(grammar_output)  ← ④ 采样    这里会进行拒绝采样 &amp; draft model的推测解码前向
       │    └─ scheduler.update_from_output()          ← ⑤ 处理结果、判断截止
       └─ post_step()
            └─ executor.take_draft_token_ids()         ← ⑥ 取出 draft tokens（同步模式）
                 └─ scheduler.update_draft_token_ids() ← ⑦ 写入 request.spec_token_ids

每个函数的循环与终止条件：
_process_input_queue：
       获取所有的请求，并处理完所有获取的请求。</description><guid isPermaLink="true">https://benyoeW.github.io/Inkwell/post/%E3%80%90vllm%E3%80%91DeepSeekMTP%E3%80%81eagle.html</guid><pubDate>Fri, 13 Mar 2026 12:30:19 +0000</pubDate></item><item><title>MLA、RoPE</title><link>https://benyoeW.github.io/Inkwell/post/MLA%E3%80%81RoPE.html</link><description>&lt;img width='2472' height='1054' alt='Image' src='https://github.com/user-attachments/assets/f11149e1-c60e-4d1f-aa8b-b3ce8e5aad9f' /&gt;
图片链接：

[https://www.bilibili.com/video/BV1fAPceeE7n/?spm_id_from=333.337.search-card.all.click&amp;vd_source=d4d8e34229b29465d437ca4e527e32c5](url)

# 理论部分：

图中的输入是一个token的embedding向量
对于Q：
将输入经过一个下采样矩阵的到latent ctq，该维度小于正常的Q向量的维度；
然后将Q经过两次上采样，得到Q合并之前的两部分（一部分有RoPE一部分没有），拼接之后得到完整的Q

对于KV：
K也是由两部分拼接而成，一部分带有RoPE，一部分没有；
带有RoPE的：token的embedding向量直接经过矩阵变换和RoPE得到
不带有RoPE的：通过下采样得到latent ctkv，latent ctkv经过一个上采样的到K的第二部分

V直接由latent ctkv经过上采样的到V


&lt;img width='2114' height='816' alt='Image' src='https://github.com/user-attachments/assets/062ea847-3529-4e93-9b56-f53885ab6d7a' /&gt;
如图，这里的
Q的Rope是通过latent C再经过一个权重矩阵得到一个向量之后，再进行Rope；
K的Rope是直接由sequence的隐藏信息经过一个权重得到特征向量再进行Rope；且是多头共享的位置特征；
# 代码实现：




。</description><guid isPermaLink="true">https://benyoeW.github.io/Inkwell/post/MLA%E3%80%81RoPE.html</guid><pubDate>Sat, 07 Mar 2026 14:10:13 +0000</pubDate></item><item><title>KV connector</title><link>https://benyoeW.github.io/Inkwell/post/KV%20connector.html</link><description>1. RequestA进入等待队列
2. 调度器发现RequestA有远程KV → 设置为WAITING_FOR_REMOTE_KVS
3. 启动异步传输，RequestA放入skipped队列
4. 调度器处理RequestB、RequestC（不阻塞！）
5. Worker异步传输KV缓存到本地
6. 下一轮调度：检查RequestA传输状态
7. 传输完成 → RequestA状态改为WAITING
8. RequestA可以进行正常调度分配


## 整体流程：
可以先看一下waiting的代码；

一个请求从sheduler侧开始：
  **某schedule轮次**：先获取远端可传输的token --&gt; 申请显存空间 --&gt; 构造远端传输的元数据 --&gt; 异步的话更改请求状态(end)
  **某schedule轮次**：若请求状态为等待KV传输--&gt;是否传输就绪--&gt;是则为申请的显存缓存KV，并更改请求状态--〉调度（end）
                                                                                                      --&gt;否则让请求进入临时队列（end）

**某update_from_output轮次**：（......）--》worker侧完成传输之后，将 KVconnector 中完成的请求放到 scheduler类中的 finished_recving_kv_req_ids 中
**某schedule轮次**：_update_waiting_for_remote_kv 检查finished_recving_kv_req_ids远程传输KV请求是否就绪，若就绪更新分配block的元数据（**传输完成后则说明该KV已经分配到对应block当中了**）

shceduler.py代码
```
        # Next, schedule the WAITING requests. ==========================================================
        if not preempted_reqs:
            while self.waiting and token_budget &gt; 0:
                if len(self.running) == self.max_num_running_reqs:
                    break

                request = self.waiting.peek_request()

                # KVTransfer: skip request if still waiting for remote kvs.
                if request.status == RequestStatus.WAITING_FOR_REMOTE_KVS:
                    # 检查远程KV是否就绪，并存入分配的显存
                    is_ready = self._update_waiting_for_remote_kv(request)
                    if is_ready:
                        if request.num_preemptions:
                            # We must be loading for a resumed preemption
                            # rather than a new request.
                            request.status = RequestStatus.PREEMPTED
                        else:
                            request.status = RequestStatus.WAITING
                    else:
                        logger.debug(
                            '%s is still in WAITING_FOR_REMOTE_KVS state.',
                            request.request_id,
                        )
                        self.waiting.pop_request()
                        # 这里放入了临时队列
                        skipped_waiting_requests.prepend_request(request)
                        continue
                        # 然后在调度其他请求结束的时候，检查临时队列，将临时队列的req放到waitting队列

                # Skip request if the structured output request is still waiting
                # for FSM compilation.
                if request.status == RequestStatus.WAITING_FOR_FSM:
                    structured_output_req = request.structured_output_request
                    if structured_output_req and structured_output_req.grammar:
                        request.status = RequestStatus.WAITING
                    else:
                        self.waiting.pop_request()
                        skipped_waiting_requests.prepend_request(request)
                        continue

                # Streaming: skip request if still waiting for next streaming req.
                if request.status == RequestStatus.WAITING_FOR_STREAMING_REQ:
                    assert not request.streaming_queue
                    self.waiting.pop_request()
                    skipped_waiting_requests.prepend_request(request)
                    continue

                # Check that adding the request still respects the max_loras
                # constraint.
                if (
                    self.lora_config
                    and request.lora_request
                    and (
                        len(scheduled_loras) == self.lora_config.max_loras
                        and request.lora_request.lora_int_id not in scheduled_loras
                    )
                ):
                    # Scheduling would exceed max_loras, skip.
                    self.waiting.pop_request()
                    skipped_waiting_requests.prepend_request(request)
                    continue

                num_external_computed_tokens = 0
                load_kv_async = False

                # Get already-cached tokens.

                # num_computed_tokens ==0 表示还没开始KV传输的相关计算
                # 抢占KVcache会抢占整个请求的，不会只抢占单个请求的部分KV，
                # 所以如果num_computed_tokens！=0，不需要connector加载远程KV
                if request.num_computed_tokens == 0:
                    # Get locally-cached tokens.
                    new_computed_blocks, num_new_local_computed_tokens = (
                        self.kv_cache_manager.get_computed_blocks(request)
                    )

                    # Get externally-cached tokens if using a KVConnector.
                    # 通过connector获取远程可以加载多少个token的KVcache
                    if self.connector is not None:
                        # 通过connector获取远程是否有已经计算的token的KVcache
                        ext_tokens, load_kv_async = (
                            self.connector.get_num_new_matched_tokens(
                                request, num_new_local_computed_tokens
                            )
                        )
                        # 表示无法确定查询情况（不是为0），暂时不调度本请求，将本请求放入临时队列
                        if ext_tokens is None:
                            # The request cannot be scheduled because
                            # the KVConnector couldn't determine
                            # the number of matched tokens.
                            self.waiting.pop_request()
                            skipped_waiting_requests.prepend_request(request)
                            continue

                        request.num_external_computed_tokens = ext_tokens
                        num_external_computed_tokens = ext_tokens

                    # Total computed tokens (local + external).
                    # 加载远程之后重新赋值num_computed_tokens
                    num_computed_tokens = (
                        num_new_local_computed_tokens + num_external_computed_tokens
                    )
                else:
                    # KVTransfer: WAITING reqs have num_computed_tokens &gt; 0
                    # after async KV recvs are completed.
                    # 在传输好KV之前就已经标记好了该请求的num_computed_tokens
                    new_computed_blocks = self.kv_cache_manager.empty_kv_cache_blocks
                    num_new_local_computed_tokens = 0
                    num_computed_tokens = request.num_computed_tokens

                encoder_inputs_to_schedule = None
                external_load_encoder_input = []
                new_encoder_compute_budget = encoder_compute_budget

                # 对于offloading connector，如果有命中远端KV，load_kv_async默认为异步，否则为同步

                # 异步加载，加载本请求的KVcache同时处理其他请求
                if load_kv_async:
                    # KVTransfer: loading remote KV, do not allocate for new work.
                    assert num_external_computed_tokens &gt; 0
                    # 这里不进行调度，只分配显存
                    num_new_tokens = 0
                    # 后面设置请求的状态为WAITING_FOR_REMOTE_KVS

                # 同步加载，获取调度的token个数
                else:
                    # Number of tokens to be scheduled.
                    # We use `request.num_tokens` instead of
                    # `request.num_prompt_tokens` to consider the resumed
                    # requests, which have output tokens.
                    num_new_tokens = request.num_tokens - num_computed_tokens
                    threshold = self.scheduler_config.long_prefill_token_threshold
                    if 0 &lt; threshold &lt; num_new_tokens:
                        num_new_tokens = threshold

                    # chunked prefill has to be enabled explicitly to allow
                    # pooling requests to be chunked
                    if (
                        not self.scheduler_config.enable_chunked_prefill
                        and num_new_tokens &gt; token_budget
                    ):
                        # If chunked_prefill is disabled,
                        # we can stop the scheduling here.
                        break

                    num_new_tokens = min(num_new_tokens, token_budget)
                    assert num_new_tokens &gt; 0

                    # Schedule encoder inputs.
                    if request.has_encoder_inputs:
                        (
                            encoder_inputs_to_schedule,
                            num_new_tokens,
                            new_encoder_compute_budget,
                            external_load_encoder_input,
                        ) = self._try_schedule_encoder_inputs(
                            request,
                            num_computed_tokens,
                            num_new_tokens,
                            encoder_compute_budget,
                            shift_computed_tokens=1 if self.use_eagle else 0,
                        )
                        if num_new_tokens == 0:
                            # The request cannot be scheduled.
                            break

                if self.need_mamba_block_aligned_split:
                    num_new_tokens = self._mamba_block_aligned_split(
                        request,
                        num_new_tokens,
                        num_new_local_computed_tokens,
                        num_external_computed_tokens,
                    )
                    if num_new_tokens == 0:
                        break

                # Handles an edge case when P/D Disaggregation
                # is used with Spec Decoding where an
                # extra block gets allocated which
                # creates a mismatch between the number
                # of local and remote blocks.
                effective_lookahead_tokens = (
                    0 if request.num_computed_tokens == 0 else self.num_lookahead_tokens
                )

                num_encoder_tokens = (
                    self._num_encoder_max_input_tokens
                    if self.is_encoder_decoder and request.has_encoder_inputs
                    else 0
                )
                # 在本地申请新的KV cache的空间
                new_blocks = self.kv_cache_manager.allocate_slots(
                    request,
                    num_new_tokens, # 总的token数量 - 已经计算KV的token数量
                    num_new_computed_tokens=num_new_local_computed_tokens, # 本地的KVcache
                    new_computed_blocks=new_computed_blocks,
                    num_lookahead_tokens=effective_lookahead_tokens,
                    num_external_computed_tokens=num_external_computed_tokens, # 远程已经计算好的KVcache
                    delay_cache_blocks=load_kv_async, # 这里应该是是否先延迟远端KVcache的加载
                    num_encoder_tokens=num_encoder_tokens,
                )

                if new_blocks is None:
                    # The request cannot be scheduled.

                    # NOTE: we need to untouch the request from the encode cache
                    # manager
                    if request.has_encoder_inputs:
                        self.encoder_cache_manager.free(request)
                    break

                # KVTransfer: the connector uses this info to determine
                # if a load is needed. Note that
                # This information is used to determine if a load is
                # needed for this request.

                # ========================================================================

                # KVcache传输触发链路：
                # scheduler端 在申请完显存之后，通过 update_state_after_alloc 将元数据传输到worker侧
                # Worker端    持续监听传输队列，执行传输，并更新scheduler端的请求状态
                # scheduler端 _update_waiting_for_remote_kv 检查远程KV是否就绪，若就绪则存入分配好的显存
                
                # ========================================================================

                if self.connector is not None:
                    # connector更新request的block，远端的KVcache加载的 Metadata
                    self.connector.update_state_after_alloc(   # 如果传输完成，update_state_after_alloc函数会直接返回
                        request,
                        self.kv_cache_manager.get_blocks(request.request_id),
                        num_external_computed_tokens,
                    )

                # Request was already popped from self.waiting
                # unless it was re-added above due to new_blocks being None.
                request = self.waiting.pop_request()

                # 如果是异步的话，更改请求的状态为WAITING_FOR_REMOTE_KVS，不进行调度，添加到临时队列中
                if load_kv_async:
                    # 如果请求远程KV传输完成，
                    # If loading async, allocate memory and put request
                    # into the WAITING_FOR_REMOTE_KV state.
                    skipped_waiting_requests.prepend_request(request)
                    # 将该请求标记为异步加载远端KV状态
                    request.status = RequestStatus.WAITING_FOR_REMOTE_KVS
                    continue   # 异步到此end

                # 如果是非异步的话，直接可以进行推理了，在执行模型前向的时候，在进行每个layer的attn计算之前等待加载完成

                self._update_connector_prefix_cache_stats(request)

                # 进行调度，添加到running队列中
                self.running.append(request)
                if self.log_stats:
                    request.record_event(
                        EngineCoreEventType.SCHEDULED, scheduled_timestamp
                    )
                if request.status == RequestStatus.WAITING:
                    scheduled_new_reqs.append(request)
                elif request.status == RequestStatus.PREEMPTED:
                    scheduled_resumed_reqs.append(request)
                else:
                    raise RuntimeError(f'Invalid request status: {request.status}')

                if self.lora_config and request.lora_request:
                    scheduled_loras.add(request.lora_request.lora_int_id)
                # 获取该请求的kv block信息
                req_to_new_blocks[request.request_id] = (
                    self.kv_cache_manager.get_blocks(request.request_id)
                )
                # 记录调度的token数量，缩减token_budget
                num_scheduled_tokens[request.request_id] = num_new_tokens
                token_budget -= num_new_tokens
                request.status = RequestStatus.RUNNING
                request.num_computed_tokens = num_computed_tokens
                # Count the number of prefix cached tokens.
                if request.num_cached_tokens &lt; 0:
                    request.num_cached_tokens = num_computed_tokens

```


**scheduler**端调用build_connector_meta()
构建需要传输的请求元数据（包括块ID、请求ID、token数量）
返回KVConnectorMetadata对象

worker侧的gpu.model_runner在前向计算之前执行：
```
        # Run model.
        if use_cudagraph:
            # Run CUDA graph.
            # NOTE(woosuk): Here, we don't need to pass the input tensors,
            # because they are already copied to the CUDA graph input buffers.

            # 绑定metadata，开始KV传输
            self.kv_connector.pre_forward(scheduler_output)

            # 请求分为KV传输与非KV传输的
            # 对于KV传输的请求，根据connector类型，决定是否异步加载
            #   若为异步传输：scheduler侧会将该请求的 num_new_tokens = 0,该请求本次不会进行前向计算
            #   若为同步传输：num_new_tokens正常计算，通过layer by layer进行前向计算
            # 对于非KV传输的请求：直接进行前向计算

            # 用cuda graph方式进行前向计算
            hidden_states = self.cudagraph_manager.run(
                input_batch.num_tokens_after_padding
            )
        else:
            .......

        # 前向执行完毕之后，
        kv_connector_output = self.kv_connector.post_forward(scheduler_output)   # 
        self.execute_model_state = hidden_states, input_batch, kv_connector_output
        return None
```

**worker**端接收到scheduler_output后：
提取kv_connector_metadata
调用bind_connector_metadata()绑定元数据
调用start_load_kv()开始异步加载
模型前向执行完毕之后调用self.kv_connector.post_forward(scheduler_output)
post_forward调用链路：
│    └── post_forward()      # 获取传输完成的请求 ID                                                    
│          │                                                                    
│         ├ ── get_finished()                                                   
│          │     └── OffloadingConnectorWorker.get_finished()   # 检查所有传输任务是否完成
│          │           └── OffloadingWorker.get_finished() 【offloading_connector.py】
│          │                 └── SingleDirectionOffloadingHandler.get_finished()【cpu_gpu.py】     # 返回完成的 job_id     
│          │                       └── event.query() 检查传输完成              
                  

## 举例：
Scheduler端：
1. 请求A分配blocks [4,5,6], 需要传输35个token
2. 构建ReqMeta(request_id='A', block_ids=[4,5,6], num_tokens=35)
3. 放入P2pNcclConnectorMetadata.requests列表

Worker端：
1. 解析metadata获取blocks [4,5,6]
2. 启动异步DMA传输：
   - block4: tokens 0-15
   - block5: tokens 16-31 
   - block6: tokens 32-34 (最后3个token)
3. 传输完成后标记请求A为可调度状态

注意点：
如果已经通过connector计算好外部token数量等信息，就直接给num_computed_token赋值了，虽说还没加载完成；

对比：
不使用OffloadingConnector：
```
# 调用链
用户 -&gt; vllm.LLM.genreate()           # 入口
      -&gt; vllm/v1/engine.py           # 引擎
      -&gt; vllm/v1/core/sched/scheduler.py  # 基础调度
      -&gt; vllm/v1/worker/model_runner.py   # 模型执行
      -&gt; vllm/attention/layers/*.py  # 注意力计算
```
使用OffloadingConnector：
```
# 调用链（扩展）
用户 -&gt; vllm.LLM.generate()          # 入口
      -&gt; vllm/v1/engine.py           # 引擎（检测KVTransferConfig）
      -&gt; vllm/v1/core/sched/scheduler.py  # 调度+Connector决策
           ↳ vllm/distributed/kv_transfer/kv_connector/factory.py
           ↳ vllm/v1/kv_offload/factory.py
           ↳ vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py
           ↳ vllm/v1/kv_offload/arc_manager.py
      
      -&gt; vllm/v1/worker/model_runner.py   # 执行+传输
           ↳ vllm/v1/worker/kv_connector_model_runner_mixin.py
           
      -&gt; vllm/attention/layers/*.py  # 注意力计算+传输同步
           ↳ vllm/attention/utils/kv_transfer_utils.py
```


connector维护了两个role：scheduler和worker

这两个role对应的方法可以被vllm中对应的结构进行调用

## scheduler
场景设定
用户请求：prompt = '北京是中国的首都，它的人口是'  (10个token)
远端 Prefill 机器 已经算过这个 prompt 的前8个token的 KV Cache
本地 已经缓存了前3个token的 KV Cache
**get_num_new_matched_tokens()**
**update_state_after_alloc()**
**build_connector_meta()**

```
Scheduler Step:

  [新请求 req_A 进来]
       ↓
  get_num_new_matched_tokens(req_A, num_computed=3)
  → 返回 (5, True)  '远端有5个token可以给你'
       ↓
  KVCacheManager 分配 block_0, block_1 给这5个token
       ↓
  update_state_after_alloc(req_A, blocks, num_external=5)
  → Connector 内部记录: 'block_0,1 要从远端拉数据'
       ↓
  ... 处理其他请求 ...
       ↓
  build_connector_meta(scheduler_output)
  → 把所有请求的传输任务打包成 metadata
  → 清空内部状态
  → 返回 meta
       ↓
  meta 被发送到 Worker 进程
       ↓
  Worker: start_load_kv() 根据 meta 真正开始传数据
```

## worker
```
═══════════════════════════════════════════════════════
                    WORKER 侧
═══════════════════════════════════════════════════════

⑤ bind_connector_metadata(meta)
  └─ self._connector_metadata = meta
  └─ Worker 现在知道要干什么了

⑥ start_load_kv(forward_context)
  └─ 读取 meta: req_A 需要从 10.0.0.1 拉 block_0, block_1
  └─ 后台线程异步开始拉数据
  └─ 立刻返回，开始前向传播

  [前向传播开始，逐层执行]

  --- Layer 0 ---
⑦  wait_for_layer_load('layer_0')
    └─ 等待 layer_0 的KV数据到达（可能短暂阻塞）
    └─ 数据到了 → 返回

    [执行 layer_0 的 Attention 计算]
    output_0 = attention_0(input, kv_cache[layer_0])

⑧  save_kv_layer('layer_0', kv_cache['layer_0'], attn_meta)
    └─ 提取 req_B 对应的KV数据
    └─ 放入发送队列，异步发送给 10.0.0.2
    └─ 立刻返回

  --- Layer 1 ---
⑦  wait_for_layer_load('layer_1')
    └─ 等 layer_1 数据... → 返回

    [执行 layer_1 的 Attention 计算]

⑧  save_kv_layer('layer_1', ...)
    └─ 异步发送 layer_1 的KV

  --- Layer 2, 3 ... 同上 ---

  [前向传播结束]

⑨ wait_for_save()
  └─ 等所有异步发送完成
  └─ 确保远端收到完整的KV数据
  └─ 返回

⑩ clear_connector_metadata()
  └─ self._connector_metadata = None
  └─ 清理完毕，准备下一次前向

═══════════════════════════════════════════════════════
                  请求结束时 (SCHEDULER 侧)
═══════════════════════════════════════════════════════

[req_A 生成完毕]

⑪ request_finished(req_A, block_ids=[block_0, block_1, block_2])
  └─ 判断是否需要把这个请求的KV异步发给别的缓存节点
  └─ 情况A: 不需要 → 返回 (False, None) → 立刻释放块
  └─ 情况B: 需要  → 返回 (True, params) → 块暂不释放
               → 后台异步发送KV
               → 发完后 get_finished() 返回 req_A
               → Scheduler 收到后才真正释放块
```

### 函数举例解析：
#### get_num_new_matched_tokens
```
举例子：
num_computed_tokens = 40
offloaded_block_size = 32
hits = 2
→ 计算: 32*(1+2)-40 = 56
→ 返回 (56, True)

request.num_tokens = 96 (总token)
offloaded_block_size = 32 (卸载块大小)
num_computed_tokens = 40 (已计算token)
hits = 2 (缓存命中2个连续块)

GPU内存状态 (已计算token: 40):
[■■■■■■■■■■■■■■■■] 卸载块0: token 0-31 (32t, 100%计算)
[■■■■□□□□□□□□□□□□] 卸载块1: token 32-63 (8/32t计算，24t待计算)

卸载存储中的命中块:（由于第0个卸载块完全计算，所以从第1个卸载块开始检查命中了多少个块，这里举例命中了2个块）
[□□□□□□□□□□□□□□□□] 卸载块1剩余: token 40-63 (缓存命中)
[□□□□□□□□□□□□□□□□] 卸载块2: token 64-95 (缓存命中)

→ 可加载56个token (24+32)
加载的时候是以token的每个layer粒度进行加载的，这里返回了token个数


def get_num_new_matched_tokens(
        self, request: Request, num_computed_tokens: int
    ) -&gt; tuple[int | None, bool]:

        # 卸载块也是有单位的，这里的一个卸载块对应多个GPU块
        # 计算请求总共需要多少个卸载块（不是GPU块）
        num_blocks = request.num_tokens // self.offloaded_block_size # num_tokens ： 请求的总token数量

        # block_size_factor：如果blocksize=16，offloaded_block_size=32，则block_size_factor为2
        assert len(request.block_hashes) // self.block_size_factor == num_blocks
        block_hashes = self._get_block_hashes(request)
        # 预热
        self.manager.touch(block_hashes)

        full_block_tokens = self.offloaded_block_size * num_blocks

        # 如果需要的token还不足一个卸载块的大小就不用搬运了
        if full_block_tokens - num_computed_tokens &lt; self.offloaded_block_size:
            # we can load less than a block, skip
            return 0, False
        # 从已经计算的token的下一个token开始寻找可加载的块
        start_block_idx = num_computed_tokens // self.offloaded_block_size

        # 返回命中的连续块数
        hits = self.manager.lookup(
            self._get_block_hashes(request, start_idx=start_block_idx)
        )
        if hits is None:
            # indicates a lookup that should be tried later
            return None, False
        if hits == 0:
            return 0, False

        num_hit_tokens = (
            self.offloaded_block_size * (start_block_idx + hits) - num_computed_tokens
        )
        logger.debug(
            'Request %s hit %s offloaded tokens after %s GPU hit tokens',
            request.request_id,
            num_hit_tokens,
            num_computed_tokens,
        )
        if num_hit_tokens &lt; self.offloaded_block_size:
            return 0, False

        if self._blocks_being_loaded:
            block_hashes = self._get_block_hashes(
                request, start_idx=start_block_idx, end_idx=start_block_idx + hits
            )

            if any(
                block_hash in self._blocks_being_loaded for block_hash in block_hashes
            ):
                # hit blocks are being loaded, delay request
                logger.debug(
                    'Delaying request %s since some of its blocks are already'
                    ' being loaded',
                    request.request_id,
                )
                return None, False

        return num_hit_tokens, True
```



## 卸载

关于KV的卸载每个step会发生什么：

### 阶段 A：Scheduler 调度（schedule()）
A1. allocate_slots 申请 GPU 显存（发生在模型前向之前）
    为新要计算的token在GPU KV cache pool 申请GPU显存，
A2. build_connector_meta 决定 Store 任务
    每一个step都为上一个step的到的新的KV cache检查一下是否有新的KVblock需要store
    TransferSpec 只是记录了'将来要从哪些 GPU block 传到哪些 CPU block'
A3. _update_after_schedule 更新 num_computed_tokens

### 阶段 B：Worker 执行（execute_model）
B1. pre_forward（model_runner.py:949 → kv_connector.py:62）
    提交上一轮积压的 store job！
    创建卸载的CUDA stream，用于KV的offload
    关键1：等待当前推理 CUDA stream 的所有计算完成，再开始 offload（利用wait）
    关键2：stream是为了确保提交的 transfer job串行
B2. 模型前向计算
    将得到的KV写入GPU显存
B3. post_forward
```
# wait_for_save → prepare_store_kv：本步 reqs_to_store 为空，无新任务
# get_finished()：
for job_id, success in self.worker.get_finished():
    # SingleDirectionOffloadingHandler.get_finished() 查询 CUDA event
    # DMA传输 完成了吗？

这里一个请求的卸载传输是否完成取决于两个条件：
1. 数据的传输是否完毕
2. 请求是否结束
只有这样都满足条件之后结束后才会触发 finished_sending → complete_store → is_ready=True

```

### 举例

#### 背景参数确认

gpu_block_size = 16 token（1 个 GPU block 存 16 个 token 的 K/V）
offloaded_block_size = 32 token（2 个 GPU block = 1 个 offloaded block）
block_size_factor = 32 / 16 = 2

prompt = 200 token
需要 GPU block 数 = ⌈200/16⌉ = 13 个（前 12 个满，第 13 个存 8 个 token）
可以凑出的完整 offloaded block = 200 // 32 = 6 个（覆盖前 192 token = 12 个 GPU block）
剩余 8 个 token（第 13 个 GPU block）不满足 offloaded_block_size，本轮不 offload

#### Step 1 结束时的状态总结
```
GPU 显存：
  Block 0~11：完整存储 token 1~192 的 KV（每层），数据已写入
  Block 12：存储 token 193~200 的 KV（每层 8/16 slot 有效），数据已写入

CPU 内存：
  CPU block 0~5：已分配（LRU manager 知道这些位置），但数据尚未写入
  （manager 中 is_ready=False，状态为 pending）

Connector 状态：
  _unsubmitted_store_jobs = [(job_id=0, TransferSpec(GPU[0..11]→CPU[0..5]))]
  _next_stored_block_idx['A'] = 6
```

#### Step 2 结束时的状态总结
```
GPU 显存：
  Block 0~11：仍保留（decode 需要读这些 KV 做 attention），数据未变
  Block 12：第 9 个 slot 已写入新 token 的 KV

CPU 内存（如果 DMA 完成）：
  CPU block 0~5：已存入 token 1~192 的全部层 KV
  （is_ready 还是 False，因为 complete_store 未被调用）
```

  → 等到请求 A 结束后触发 finished_sending → complete_store → is_ready=True


**卸载的时候会检查block的唯一性，只保存不存在的KVcache；**






# 模块设计
采用 抽象-实现-工厂 的模式
层1: 抽象接口层 distributed/kv_transfer/kv_connector/v1/base.py
层2: 工厂层 kv_connector/factory.py
层3: 具体实现层 kv_connector/v1/offloading_connector.py
层4: Offloading 专属子系统 v1/kv_offload/
        Connector 实现的内部引擎，仅被 offloading_connector.py 使用

①  import factory.py
    → register_connector('OffloadingConnector', ...) 写入注册表
    → offloading_connector.py 此时未被加载
在vllm启动时会加载scheduler，scheduler内自动 import factory.py；

②  Scheduler.__init__()
    → create_connector(role=SCHEDULER)
      → importlib 首次加载 offloading_connector.py
      → OffloadingConnector.__init__ → CPUOffloadingSpec（计算块数）
      → OffloadingConnectorScheduler → CPUBackend + LRUOffloadingManager

③  Worker.initialize_from_config()  （KV cache 内存分配完成后）
    → ensure_kv_transfer_initialized()
    → create_connector(role=WORKER)
      → OffloadingConnector.__init__ → CPUOffloadingSpec（计算块数）
      → OffloadingConnectorWorker → OffloadingWorker（空路由表）

④  gpu_model_runner.initialize_kv_cache()
    → get_kv_connector() → ActiveKVConnector
    → register_kv_caches()
      → CpuGpuOffloadingHandlers 创建
      → 分配 pin memory、初始化 CUDA stream 池  ← 真正的 GPU/CPU 资源在此分配


。</description><guid isPermaLink="true">https://benyoeW.github.io/Inkwell/post/KV%20connector.html</guid><pubDate>Fri, 06 Mar 2026 11:02:42 +0000</pubDate></item><item><title>profilling</title><link>https://benyoeW.github.io/Inkwell/post/profilling.html</link><description># torch profiller
## 采集流程
### sglang
服务端：
1. 开启sglang服务
2. 进行压测

客户端：
开启压测之后，在压测过程中采用以下指令
export SGLANG_TORCH_PROFILER_DIR=/sgl-workspace/sglang/trace_file // 这是保存采集文件的目标路径，如果不设置一般会保存到/tmp
curl -X POST http://127.0.0.1:8000/start_profile  // 开启的开关
curl -X POST http://127.0.0.1:8000/stop_profile  // 关闭的开关
采集的文件一般以*.trace.json.gz命名，若多个卡则有多个该文件
注意：采集了10s左右，大概3个G大小
### vllm
【待施工】
## hta
官方参考 https://docs.pytorch.org/tutorials/beginner/hta_intro_tutorial.html
`from hta.trace_analysis import TraceAnalysis`
&gt; 采集文件的文件夹的地址，一般多个文件都放在一个文件夹内，hta会自动遍历这些文件
`trace_dir = '/Users/xawei/wxx_workspace/profilling/target_file_dir'`  

`analyzer = TraceAnalysis(trace_dir=trace_dir)`

下面是一些可视化方法，运行之后会打开浏览器自动显示hta分析信息

&gt; time_spent_df = analyzer.get_temporal_breakdown()
&gt; idle_time_df = analyzer.get_idle_time_breakdown()
&gt; kernel_type_metrics_df, kernel_metrics_df = analyzer.get_gpu_kernel_breakdown()
&gt; overlap_df = analyzer.get_comm_comp_overlap()




# nsys
。</description><guid isPermaLink="true">https://benyoeW.github.io/Inkwell/post/profilling.html</guid><pubDate>Tue, 03 Mar 2026 06:10:47 +0000</pubDate></item><item><title>【vllm】 线上模式</title><link>https://benyoeW.github.io/Inkwell/post/%E3%80%90vllm%E3%80%91%20-xian-shang-mo-shi.html</link><description># APIserver在框架中的作用
## 调用链路
vLLM的online serving采用分层架构，从HTTP接口到核心推理引擎的完整链路如下：

客户端请求 → HTTP服务器 → API路由 → 服务层 → 引擎客户端 → 核心引擎 → 推理执行

具体文件链路：
examples/online_serving/openai_chat_completion_client.py (客户端)
    ↓
vllm/entrypoints/openai/api_server.py (HTTP服务器入口)          压测其服务的文件地址
    ↓  
vllm/entrypoints/openai/chat_completion/api_router.py (API路由)
    ↓
vllm/entrypoints/openai/chat_completion/serving.py (服务层)
    ↓
vllm/v1/engine/async_llm.py (异步引擎客户端)
    ↓
vllm/v1/engine/core_client.py (引擎核心客户端)
    ↓
vllm/v1/engine/core.py (核心引擎)
    ↓
vllm/v1/executor/ (推理执行器)


客户端 (examples/online_serving/openai_chat_completion_client.py)
    ↓ HTTP请求到 http://localhost:8000/v1/chat/completions
服务器 (vllm/entrypoints/openai/api_server.py)
    ↓ 路由到 vllm/entrypoints/openai/chat_completion/api_router.py
    ↓ 调用 vllm/entrypoints/openai/chat_completion/serving.py
    ↓ 委托给 vllm/v1/engine/async_llm.py

## 服务模式和程序模式的区别：
场景 1：前端网页要用模型👉 那只能走 HTTP  
场景 2：很多人同时用模型👉 模型必须只加载一次  
场景 3：模型要一直开着（7×24）👉 不适合做服务  
API Server 就是为了解决上面这些问题  

## 什么是 FastAPI？
如果没有 FastAPI，你要手写很多麻烦的东西：
解析 HTTP
解析 JSON
校验参数
返回结果
FastAPI 帮你全做了。</description><guid isPermaLink="true">https://benyoeW.github.io/Inkwell/post/%E3%80%90vllm%E3%80%91%20-xian-shang-mo-shi.html</guid><pubDate>Fri, 06 Feb 2026 08:11:47 +0000</pubDate></item><item><title>gpu coding&amp;arch</title><link>https://benyoeW.github.io/Inkwell/post/gpu%20coding%26arch.html</link><description># gemm
```c++
#include &lt;cuda_runtime.h&gt;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

# define BLOCKSIZE 16

__global__ void gemm_kernel_tile(const float* __restrict__ a,
                                  const float* __restrict__ b,
                                  float* __restrict__ c,
                                  int M, int N, int K){
                                
                                int y = blockIdx.y * blockDim.y + threadIdx.y;
                                int x = blockIdx.x * blockDim.x + threadIdx.x;
                                
                                __shared__ float a_tile[BLOCKSIZE][BLOCKSIZE], b_tile[BLOCKSIZE][BLOCKSIZE];
                                
                                float sum = 0.0f;

                                for(int i=0;i&lt;K/BLOCKSIZE;++i){
                                    // load to sahared memory a
                                    int ax = threadIdx.x + i * BLOCKSIZE;
                                    int ay = y;
                                    if(ax &lt; K &amp;&amp; ay &lt; M){
                                        a_tile[threadIdx.y][threadIdx.x] = a[ay * K + ax];   // ⭐️ 防止共享内存坐标越界
                                    }else{
                                        a_tile[threadIdx.y][threadIdx.x] = 0.0f;
                                    }
                                    // load to sahared memory b
                                    int bx = threadIdx.x;
                                    int by = threadIdx.y + i * BLOCKSIZE;
                                    if(bx &lt; N &amp;&amp; by &lt; K){
                                        b_tile[threadIdx.y][threadIdx.x] = b[by * N + bx];
                                    }else{
                                        b_tile[threadIdx.y][threadIdx.x] = 0.0f;
                                    }

                                    __syncthreads();  // ⭐️ 数据加载完成之后同步

                                    
                                    for(int i=0;i&lt;BLOCKSIZE;++i){
                                        sum += a_tile[threadIdx.y][i] * b_tile[i][threadIdx.x];
                                    }
                                    __syncthreads();  // ⭐️ 同步，防止提前进入下一轮计算然后累加错误
                                }
                                if(x &lt; N &amp;&amp; y &lt; M){
                                    c[y * N + x] = sum;
                                }
                            }
                                    


__global__ void gemm_kernel(const float* __restrict__ a,
                            const float* __restrict__ b,
                            float* __restrict__ c,
                            int M, int N, int K) {
    // Each thread computes one element of C
    int row = blockIdx.y * blockDim.y + threadIdx.y; // y -&gt; row in C (0..M-1)
    int col = blockIdx.x * blockDim.x + threadIdx.x; // x -&gt; col in C (0..N-1)

    if (row &lt; M &amp;&amp; col &lt; N) {
        float sum = 0.0f;
        for (int k = 0; k &lt; K; ++k) {
            // A[row][k] * B[k][col]
            sum += a[row * K + k] * b[k * N + col];
        }
        c[row * N + col] = sum;
    }
}

int main() {
    // Matrix dimensions: A(MxK) * B(KxN) = C(MxN)
    const int M = 5000;
    const int K = 6000;
    const int N = 4000;

    const size_t size_a = M * K * sizeof(float);
    const size_t size_b = K * N * sizeof(float);
    const size_t size_c = M * N * sizeof(float);

    // Host memory allocation
    float *h_a = (float*)malloc(size_a);
    float *h_b = (float*)malloc(size_b);
    float *h_c = (float*)malloc(size_c);
    float *h_c_ref = (float*)malloc(size_c); // Optional: CPU reference

    // Initialize host matrices
    for (int i = 0; i &lt; M * K; ++i) h_a[i] = 1.0f; // A all 1s
    for (int i = 0; i &lt; K * N; ++i) h_b[i] = 2.0f; // B all 2s
    for (int i = 0; i &lt; M * N; ++i) h_c[i] = 0.0f; // Initialize to 0

    // Device memory allocation
    float *d_a, *d_b, *d_c;
    cudaError_t err;

    err = cudaMalloc(&amp;d_a, size_a);
    if (err != cudaSuccess) { fprintf(stderr, 'cudaMalloc d_a failed: %s\n', cudaGetErrorString(err)); return 1; }

    err = cudaMalloc(&amp;d_b, size_b);
    if (err != cudaSuccess) { fprintf(stderr, 'cudaMalloc d_b failed: %s\n', cudaGetErrorString(err)); return 1; }

    err = cudaMalloc(&amp;d_c, size_c);
    if (err != cudaSuccess) { fprintf(stderr, 'cudaMalloc d_c failed: %s\n', cudaGetErrorString(err)); return 1; }

    // Copy data from host to device
    cudaMemcpy(d_a, h_a, size_a, cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, h_b, size_b, cudaMemcpyHostToDevice);
    cudaMemcpy(d_c, h_c, size_c, cudaMemcpyHostToDevice);

    // Kernel launch configuration
    dim3 blockSize(16, 16); // 256 threads per block
    dim3 gridSize((N + blockSize.x - 1) / blockSize.x,
                  (M + blockSize.y - 1) / blockSize.y);

    // Launch kernel
    gemm_kernel_tile&lt;&lt;&lt;gridSize, blockSize&gt;&gt;&gt;(d_a, d_b, d_c, M, N, K);

    // Check for kernel launch errors
    err = cudaGetLastError();
    if (err != cudaSuccess) {
        fprintf(stderr, 'Kernel launch failed: %s\n', cudaGetErrorString(err));
        return 1;
    }

    // Wait for GPU to finish
    cudaDeviceSynchronize();

    // Copy result back to host
    cudaMemcpy(h_c, d_c, size_c, cudaMemcpyDeviceToHost);

    printf('First 10 elements of C (row 0, columns 0～9):\n');
    for (int i = 0; i &lt; 10 &amp;&amp; i &lt; N; ++i) {
        printf('C[0][%d] = %.2f\n', i, h_c[i]);
    }
    printf('\n');

    // Optional: Verify result (C should be all 2*K = 400.0f)
    bool correct = true;
    float expected = 2.0f * K; // since A=1, B=2, sum over K terms: 1*2*K
    for (int i = 0; i &lt; M * N; ++i) {
        if (abs(h_c[i] - expected) &gt; 1e-5) {
            correct = false;
            break;
        }
    }

    printf('Matrix multiplication result: %s\n', correct ? 'PASSED' : 'FAILED');
    if (!correct) {
        printf('Example: h_c[0] = %f, expected = %f\n', h_c[0], expected);
    }

    // Cleanup
    free(h_a); free(h_b); free(h_c); free(h_c_ref);
    cudaFree(d_a); cudaFree(d_b); cudaFree(d_c);
    // no
    return 0;
}

```

# transpose
```c++
# include &lt;stdio.h&gt;
# include &lt;math.h&gt;

#define BLOCK_SIZE 32
#define M 3000
#define N 1000

__managed__ int matrix[N][M];
__managed__ int gpu_result[M][N];
__managed__ int cpu_result[M][N];

__global__ void gpu_matrix_transpose(int in[N][M], int out[M][N])
{
    int x = threadIdx.x + blockDim.x * blockIdx.x;
    int y = threadIdx.y + blockDim.y * blockIdx.y;

    if( x &lt; M &amp;&amp; y &lt; N)
    {
        out[x][y] = in[y][x];
    }
}

// 创建m行，n列的线程数量【由多个线程块组成的】
__global__ void gpu_shared_matrix_transpose(int in[N][M], int out[M][N])
{

    int y = threadIdx.y + blockDim.y * blockIdx.y;
    int x = threadIdx.x + blockDim.x * blockIdx.x;

    __shared__ int ken[BLOCK_SIZE+1][BLOCK_SIZE+1];//ken[32] warp

    // step1：
    if(x &lt; M &amp;&amp; y &lt; N)
    {   
        // step1：读到共享内存
        ken[threadIdx.y][threadIdx.x] = in[y][x];
    }
    __syncthreads();

    // 原则：相邻的线程访问相邻的坐标

    // step2：  块反转，块内坐标不变
    int x1 = threadIdx.x + blockDim.y * blockIdx.y;
    int y1 = threadIdx.y + blockDim.x * blockIdx.x;
    
    if(x1 &lt; N &amp;&amp; y1 &lt; M)
    {
    // step3：从共享内存读到输出数据
        out[y1][x1] = ken[threadIdx.x][threadIdx.y];//32 bank
    }

}

void cpu_matrix_transpose(int in[N][M], int out[M][N])
{
    for(int y = 0; y &lt; N; y++)
    {
        for(int x = 0; x &lt; M; x++)
        {
            out[x][y] = in[y][x];
        }
    }
}

int main()
{
    for(int y=0; y&lt;N; y++)
    {
        for(int x=0; x&lt;M; x++)
        {
            matrix[y][x] = rand()%1024;
        }
    }

    cudaEvent_t start, stop_gpu, stop_cpu;
    cudaEventCreate(&amp;start);
    cudaEventCreate(&amp;stop_cpu);
    cudaEventCreate(&amp;stop_gpu);

    cudaEventRecord(start);
    cudaEventSynchronize(start);

    dim3 dimGrid((M + BLOCK_SIZE - 1)/BLOCK_SIZE, (N + BLOCK_SIZE -1)/BLOCK_SIZE);
    dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);

    for(int i = 0; i &lt; 20; i++)
    {
        // gpu_matrix_transpose&lt;&lt;&lt;dimGrid,dimBlock&gt;&gt;&gt;(matrix, gpu_result);
        gpu_shared_matrix_transpose&lt;&lt;&lt;dimGrid,dimBlock&gt;&gt;&gt;(matrix, gpu_result);
        cudaDeviceSynchronize();
    }

    cudaEventRecord(stop_gpu);
    cudaEventSynchronize(stop_gpu);

    cpu_matrix_transpose(matrix, cpu_result);

    cudaEventRecord(stop_cpu);
    cudaEventSynchronize(stop_cpu);

    float time_cpu, time_gpu;
    cudaEventElapsedTime(&amp;time_gpu, start, stop_gpu);
    cudaEventElapsedTime(&amp;time_cpu, stop_gpu, stop_cpu);

    bool errors = false;
    for(int y = 0; y&lt;M; y++)
    {
        for (int x = 0; x &lt; N; x++)
        {
            if(fabs(cpu_result[y][x] - gpu_result[y][x]) &gt; (1.0e-10))
            {
                errors = true;
            }
        }
        
    }

    printf('Result: %s\n', errors?'Error':'Pass');
    printf('CPU time: %.2f\nGPU time: %.2f\n', time_cpu, time_gpu/20.0);

    return 0;
}
```。</description><guid isPermaLink="true">https://benyoeW.github.io/Inkwell/post/gpu%20coding%26arch.html</guid><pubDate>Sun, 25 Jan 2026 08:57:14 +0000</pubDate></item></channel></rss>