<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>YFGUG</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://luanjiahao.me/</id>
  <link href="https://luanjiahao.me/" rel="alternate"/>
  <link href="https://luanjiahao.me/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, YFGUG</rights>
  <subtitle>Notes from the trail and everyday life</subtitle>
  <title>
    <![CDATA[Travel & Life]]>
  </title>
  <updated>2026-10-28T04:22:26.309Z</updated>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE/"/>
    <category term="海光 DCU" scheme="https://luanjiahao.me/tags/%E6%B5%B7%E5%85%89-DCU/"/>
    <category term="vLLM" scheme="https://luanjiahao.me/tags/vLLM/"/>
    <category term="Attention" scheme="https://luanjiahao.me/tags/Attention/"/>
    <category term="KV Cache" scheme="https://luanjiahao.me/tags/KV-Cache/"/>
    <category term="长上下文" scheme="https://luanjiahao.me/tags/%E9%95%BF%E4%B8%8A%E4%B8%8B%E6%96%87/"/>
    <content>
      <![CDATA[<h2 id="Abstract"><a href="#Abstract" class="headerlink" title="Abstract"></a>Abstract</h2><p>In large-model inference, the Attention formula is usually not the hardest part; what really determines performance is often how data lands in memory and at what granularity the hardware can fetch that data at once.</p><p>This article uses Qwen3.5’s official prefix-off long-context path as an example to discuss a problem that seems small but actually runs through vLLM configuration, KV Cache, paged attention, and DCU kernel dispatch: the default physical KV Cache page on this path is 784 tokens, while the vendor paged-attention path on Hygon DCU gfx936 requires the page length to satisfy 64-token alignment. In the end, we did not hard-code page size as a scheduler parameter; instead, following vLLM’s existing hybrid Attention&#x2F;Mamba page calculation logic, we automatically derived it to the minimum compatible value 832.</p><p>This change itself differs only by one number, but it changes the precondition for whether the kernel can be invoked. Subsequently, chunked-prefill with q&#x3D;4096 enters the vendor FlashAttention path, while q&#x3D;1 decode continues to use the page-contiguous Triton path, avoiding mixing two completely different workloads into one kernel.</p><p>This article focuses on DCU execution granularity, KV Cache physical layout, block table address calculation, and end-to-end validation. It does not cover competition submission, repository addresses, or internal environment details.</p><hr><h2 id="1-Why-Start-Looking-at-Attention-from-the-“Page”"><a href="#1-Why-Start-Looking-at-Attention-from-the-“Page”" class="headerlink" title="1. Why Start Looking at Attention from the “Page”"></a>1. Why Start Looking at Attention from the “Page”</h2><p>For a context of length <code>T</code>, Attention needs to access historical K&#x2F;V. vLLM does not allocate one contiguous K&#x2F;V block per request; instead, it slices the cache into fixed-size physical pages and maps logical pages to physical pages via the block table.</p><p>The address of a token can be simplified as:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">logical_page = token_index // BLOCK_SIZE</span><br><span class="line">page_offset  = token_index %  BLOCK_SIZE</span><br><span class="line">physical_page = block_table[logical_page]</span><br></pre></td></tr></table></figure><p>Then combine this with the K&#x2F;V cache stride, KV head, and head dimension to calculate the final address.</p><p>This paged design solves memory fragmentation and dynamic request management, but it also brings a hardware fact: <strong>BLOCK_SIZE is not just an integer in the scheduler; it also determines the kernel’s intra-page loop, vectorized access, and boundary mask.</strong></p><p>If the hardware kernel wants to use 64 tokens as a regular access unit, then a physical page not divisible by 64 usually leaves an irregular tile or extra boundary handling at the tail of each page. For short requests, this may be only a small waste; for long contexts, page-table parsing and K&#x2F;V reads are repeated many times.</p><h2 id="2-Key-Constraint-of-gfx936-wave64-and-64-Token-Access-Granularity"><a href="#2-Key-Constraint-of-gfx936-wave64-and-64-Token-Access-Granularity" class="headerlink" title="2. Key Constraint of gfx936: wave64 and 64-Token Access Granularity"></a>2. Key Constraint of gfx936: wave64 and 64-Token Access Granularity</h2><p>The ROCm-compatible execution environment of Hygon DCU uses wave64 execution granularity on the gfx936 path. Here wave64 should not be simplified to “the number of threads becomes 64”; it affects:</p><ul><li>how threads within a wave cooperate to load vectors;</li><li>how the token dimension of K&#x2F;V is assigned to lanes;</li><li>whether contiguous addresses can form regular memory transactions;</li><li>how the kernel handles page-tail masks and cross-page access.</li></ul><p>In the vendor paged-attention interface used in this experiment, the physical page length must satisfy 64-token alignment. What is directly confirmed by source code and runtime logs is this interface constraint; it matches the access granularity of wave64, but this article does not attribute 64 alignment solely to wave64. The default 784 can be decomposed as:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">784 = 12 × 64 + 16</span><br></pre></td></tr></table></figure><p>and 832 can be decomposed as:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">832 = 13 × 64</span><br></pre></td></tr></table></figure><p>This means page832 can internally consist of 13 complete 64-token access groups, without leaving an irregular 16-token tail block at the end of each physical page. The benefit here does not come from reducing Attention mathematical computation, but from aligning data layout with hardware execution granularity.</p><p>It should be emphasized that page832 is not an empirical parameter of “bigger is faster”. 1024 also satisfies 64 alignment, but it has about 30.6% more token capacity than 784; 832 increases by only about 6.1% and is the minimum compatible page satisfying the constraint, more suitable for memory-constrained 27B hybrid models.</p><h2 id="3-Why-Qwen3-5’s-Hybrid-Cache-Makes-This-Problem-More-Complex"><a href="#3-Why-Qwen3-5’s-Hybrid-Cache-Makes-This-Problem-More-Complex" class="headerlink" title="3. Why Qwen3.5’s Hybrid Cache Makes This Problem More Complex"></a>3. Why Qwen3.5’s Hybrid Cache Makes This Problem More Complex</h2><p>Qwen3.5 contains both full attention and GDN&#x2F;Mamba-like state paths. vLLM needs to keep the Attention page and Mamba state page compatible in the hybrid model; otherwise, the same request encounters different cache granularity and padding rules across layers.</p><p>On the path with prefix caching disabled, page selection can be abstracted as (omitting the minimum comparison and subsequent padding branches in the existing configuration):</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">attn_page_size_1_token = FullAttentionSpec(...).page_size_bytes</span><br><span class="line">required_page = ceil(mamba_page_bytes /</span><br><span class="line">                     (alignment_tokens * attn_page_size_1_token))</span><br><span class="line">                 * alignment_tokens</span><br></pre></td></tr></table></figure><p>For this Qwen3.5&#x2F;DCU shape, backend adaptation raises <code>alignment_tokens</code> from the generic 16 to 64; after calculation along the existing <code>verify_and_update_config</code> logic, the default page is rounded up from 784 to 832.</p><p>This is two different things from directly writing <code>block_size = 832</code> in the scheduler:</p><ol><li>The calculation is still completed by existing model configuration and cache-spec data flow;</li><li>Page size is still passed to subsequent components as the result of KV Cache physical layout;</li><li>The scheduler’s token budget, chunk count, and request scheduling semantics remain unchanged;</li><li>When Qwen3.5, ROCm, head size, KV head count, and prefix state conditions are not met, the original logic is preserved.</li></ol><p>The core of such a change is “letting the backend’s physical constraints enter the existing layout calculation”, rather than bypassing vLLM’s cache model with a new parameter.</p><h2 id="4-Actual-Page-Shape-of-Qwen3-5"><a href="#4-Actual-Page-Shape-of-Qwen3-5" class="headerlink" title="4. Actual Page Shape of Qwen3.5"></a>4. Actual Page Shape of Qwen3.5</h2><p>The full-attention shape in this experiment is highly deterministic:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">query heads: 24</span><br><span class="line">KV heads:     4</span><br><span class="line">GQA ratio:    6</span><br><span class="line">head size:    256</span><br><span class="line">dtype:        BF16</span><br></pre></td></tr></table></figure><p>vLLM’s complete K&#x2F;V cache tensor also includes a K&#x2F;V dimension; the shape can be written as:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[num_physical_pages, 2, BLOCK_SIZE, 4, 256]</span><br></pre></td></tr></table></figure><p>After <code>unbind(1)</code>, the shape of an individual K or V cache is:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[num_physical_pages, BLOCK_SIZE, 4, 256]</span><br></pre></td></tr></table></figure><p>When K and V are stored separately, the data volume of a single page is approximately:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">BLOCK_SIZE × 4 × 256 × sizeof(BF16)</span><br></pre></td></tr></table></figure><p>For K and V together, multiply by 2. After switching to page832, a single page holds a few more tokens, but every 64-token group within the page is complete. This increased capacity cost must be evaluated together with kernel memory access efficiency; one cannot look only at single-page latency.</p><h2 id="5-From-Page-Table-to-Coalesced-Load"><a href="#5-From-Page-Table-to-Coalesced-Load" class="headerlink" title="5. From Page Table to Coalesced Load"></a>5. From Page Table to Coalesced Load</h2><p>The core loop of a generic Triton paged-attention kernel can be abstracted as follows:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> tile <span class="keyword">in</span> key_tiles:</span><br><span class="line">    token = tile * TILE_SIZE + lane_offset</span><br><span class="line">    logical_page = token // BLOCK_SIZE</span><br><span class="line">    page_offset = token % BLOCK_SIZE</span><br><span class="line">    physical_page = block_table[logical_page]</span><br><span class="line"></span><br><span class="line">    K = load(key_cache[physical_page, page_offset, kv_head, dim])</span><br><span class="line">    V = load(value_cache[physical_page, page_offset, kv_head, dim])</span><br><span class="line">    score = dot(Q, K) * scale</span><br><span class="line">    update_online_softmax(score, V)</span><br></pre></td></tr></table></figure><p>On page784, the result of <code>token % BLOCK_SIZE</code> leaves an interval at the page tail that cannot be aligned with a 64-token access group. A generic kernel can guarantee correctness via mask, but mask does not turn irregular addresses into regular addresses; what is described here is mechanistic inference of memory-access layout, not additional hardware trace counts.</p><p>On page832, the token interval within a page can naturally be divided into complete 64-token groups. The vendor kernel still needs to handle the actual length of the last request, but it does not need to face a fixed 16-token tail block inside every physical page.</p><p>The optimization point here is not “removing the causal mask”. The causal constraint is fully preserved; what changes is the intra-page loop and physical address layout, making vector loads better suited to DCU execution granularity.</p><h2 id="6-Why-the-Vendor-Kernel-Only-Takes-Over-Strict-Single-Sequence-Prefill-Shapes"><a href="#6-Why-the-Vendor-Kernel-Only-Takes-Over-Strict-Single-Sequence-Prefill-Shapes" class="headerlink" title="6. Why the Vendor Kernel Only Takes Over Strict Single-Sequence Prefill Shapes"></a>6. Why the Vendor Kernel Only Takes Over Strict Single-Sequence Prefill Shapes</h2><p>Long-context serving is not a single workload. q&#x3D;1 decode, q&#x3D;4096 chunked-prefill, and the final tail chunk of less than 4096 differ in parallelism, memory-access direction, and graph capture method.</p><p>Therefore, the vendor dispatch of the final Tail-Q version uses strict conditions rather than global replacement:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">1 &lt; num_actual_tokens &lt;= 4096</span><br><span class="line">max_seqlen_q == num_actual_tokens</span><br><span class="line">single sequence</span><br><span class="line">decoder attention</span><br><span class="line">BF16 Q/K/V/output</span><br><span class="line">Qwen3.5: 24 Q heads, 4 KV heads, head size 256</span><br><span class="line">page size = 832</span><br><span class="line">GQA ratio = 6</span><br><span class="line">causal = true</span><br><span class="line">no sliding window</span><br><span class="line">no ALiBi / sinks / softcap</span><br><span class="line">no multimodal prefix range</span><br></pre></td></tr></table></figure><p>When the conditions are met, call the existing vendor <code>varlen_fwd_unified</code> interface:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">q, k, v</span><br><span class="line">cu_seqlens_q</span><br><span class="line">seqused_k</span><br><span class="line">block_table</span><br><span class="line">max_seqlen_q / max_seqlen_k</span><br><span class="line">causal = true</span><br></pre></td></tr></table></figure><p>If not met, fall back to the original unified Triton attention. This design has two engineering implications:</p><ul><li>The vendor kernel only faces the fixed shapes it is really good at;</li><li>Even if the new path is unsuitable at some boundary, it will not pollute generic attention.</li></ul><p>The initial candidate only covered q&#x3D;4096; the final Tail-Q version covers <code>1 &lt; q &lt;= 4096</code> and <code>max_seqlen_q == q</code>, explicitly excluding q&#x3D;1 decode. Thus the last prefill chunk of less than 4096 can also enter the same class of full causal attention, without changing the scheduler’s chunking logic.</p><h2 id="7-Why-Decode-Needs-a-Separately-Maintained-Page-Contiguous-Path"><a href="#7-Why-Decode-Needs-a-Separately-Maintained-Page-Contiguous-Path" class="headerlink" title="7. Why Decode Needs a Separately Maintained Page-Contiguous Path"></a>7. Why Decode Needs a Separately Maintained Page-Contiguous Path</h2><p>prefill has a large q, suitable for letting vendor FlashAttention handle an entire query chunk; decode q is usually 1, and the main problem becomes reading K&#x2F;V from a large number of historical pages and completing online softmax with low launch overhead.</p><p>The loop of the page-contiguous decode kernel is closer to:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> page_idx <span class="keyword">in</span> pages_for_sequence:</span><br><span class="line">    physical_page = block_table[page_idx]</span><br><span class="line">    <span class="keyword">for</span> local_tile <span class="keyword">in</span> tiles_inside_page:</span><br><span class="line">        page_offset = local_tile * TILE_SIZE + offsets</span><br><span class="line">        K = load(K_cache[physical_page, page_offset, ...])</span><br><span class="line">        V = load(V_cache[physical_page, page_offset, ...])</span><br><span class="line">        update_online_softmax(Q, K, V)</span><br></pre></td></tr></table></figure><p>This path still parses the block table; it does not skip any logical page, nor change the causal range. It only reduces generic scheduling overhead for the access geometry of q&#x3D;1, GQA6, head size 256, and supports both page784 and page832.</p><p>On page832, the decode speedup of the standalone CUDAGraph microbench is approximately:</p><table><thead><tr><th align="right">KV length</th><th align="right">Generic page832</th><th align="right">page-contiguous</th><th align="right">Speedup</th></tr></thead><tbody><tr><td align="right">8192</td><td align="right">0.115989 ms</td><td align="right">0.080010 ms</td><td align="right">1.45x</td></tr><tr><td align="right">16384</td><td align="right">0.154312 ms</td><td align="right">0.122411 ms</td><td align="right">1.26x</td></tr><tr><td align="right">32768</td><td align="right">0.405256 ms</td><td align="right">0.278832 ms</td><td align="right">1.45x</td></tr></tbody></table><p>This set of results is bitwise exact; however, the speedup of an independent kernel cannot be directly treated as end-to-end serving speedup, because decode is also surrounded by Linear, sampling, graph replay, and Python&#x2F;scheduling boundaries.</p><h2 id="8-Measured-Results-How-Kernel-Speedup-Propagates-to-the-Serving-Layer"><a href="#8-Measured-Results-How-Kernel-Speedup-Propagates-to-the-Serving-Layer" class="headerlink" title="8. Measured Results: How Kernel Speedup Propagates to the Serving Layer"></a>8. Measured Results: How Kernel Speedup Propagates to the Serving Layer</h2><p>Under the same container, same model, same native extension, and same dataset, baseline uses the official prefix-off page784 path, candidate uses page832, vendor prefill, and page-contiguous decode. Each side completed 10&#x2F;10 for all three tiers, failed&#x3D;0. Runtime logs also confirm four key facts:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">baseline: prefix caching = False, block size = 784</span><br><span class="line">candidate: prefix caching = False, block size = 832</span><br><span class="line">vendor q=4096 prefill marker: hit</span><br><span class="line">page832 decode marker: hit</span><br></pre></td></tr></table></figure><p>The relative changes of the local three-tier A&#x2F;B are:</p><table><thead><tr><th>Context tier</th><th align="right">Throughput change</th></tr></thead><tbody><tr><td>4K-8K</td><td align="right">+0.62%</td></tr><tr><td>8K-16K</td><td align="right">+2.67%</td></tr><tr><td>16K-32K</td><td align="right">+13.78%</td></tr></tbody></table><p>q&#x3D;4096 vendor prefill standalone is about 2.75x-2.92x relative to the page784 Triton path; the improvement in full serving is significantly smaller than this number. The long tier has more obvious gains because in this test workload, q&#x3D;4096 chunks and long-sequence paged-KV access account for a higher proportion; this is an explanation derived from workload&#x2F;profile and should not be understood as a single trace already proving the exact proportion of a certain memory transaction.</p><p>This also explains why one cannot use only a kernel table as the conclusion: in the official 8K profile of page832, prefix-off, all-Tail-Q, the total Attention bucket of q&#x3D;4096 prefill drops from 681.071 ms to 238.718 ms; this is not the standalone time of a single vendor kernel, and end-to-end serving still pays GEMM, GDN, decode, and scheduling costs.</p><p>The numerical scope here must be separated: 2.75x-2.92x is standalone kernel comparison, +0.62%&#x2F;+2.67%&#x2F;+13.78% is same-container end-to-end A&#x2F;B, 681.071&#x2F;238.718 ms is the profile’s total Attention bucket. They cannot substitute for each other, nor can local A&#x2F;B ratios be directly treated as official absolute throughput on another machine.</p><h2 id="9-Numerical-Correctness-Fast-Does-Not-Mean-Bitwise-Identical"><a href="#9-Numerical-Correctness-Fast-Does-Not-Mean-Bitwise-Identical" class="headerlink" title="9. Numerical Correctness: Fast Does Not Mean Bitwise Identical"></a>9. Numerical Correctness: Fast Does Not Mean Bitwise Identical</h2><p>vendor BF16 attention and Triton attention compute the same complete causal Attention, but the parallel reduction order may differ. Therefore:</p><ul><li>vendor standalone output is finite and stable across repeated runs;</li><li>page832 Triton and page784 Triton can be bitwise identical;</li><li>relative L2 error between vendor and Triton is about <code>0.0058-0.0064</code>;</li><li>generated text is not guaranteed to be bitwise identical per request.</li></ul><p>This is not a detail that can be ignored. Tiny differences in BF16 reduction may be amplified in long-sequence generation, so one must simultaneously do:</p><ol><li>kernel output finite check;</li><li>repeated-run consistency check;</li><li>relative error comparison with reference kernel;</li><li>four types of task-level accuracy smoke;</li><li>record generated text and token count per request.</li></ol><p>In this route, Q&#x2F;K&#x2F;V are not skipped, the causal range is not shortened, no sparse attention is done, and no cross-request KV reuse is done; the changes are physical page layout and equivalent kernel selection.</p><h2 id="10-An-Important-Failure-Case-page832-Once-“Seemed-Ineffective”"><a href="#10-An-Important-Failure-Case-page832-Once-“Seemed-Ineffective”" class="headerlink" title="10. An Important Failure Case: page832 Once “Seemed Ineffective”"></a>10. An Important Failure Case: page832 Once “Seemed Ineffective”</h2><p>The most valuable finding of this route is not the first 2.9x result, but the later discovery that the test path behind the old conclusion was incomplete.</p><p>The old experiment only submitted the attention dispatch change, but did not simultaneously submit the page-layout alignment logic. The official loader disabled prefix caching, and the actual runtime was still page784; because 784 does not satisfy the vendor kernel’s 64-token page requirement, the vendor path did not hit at all.</p><p>On the other hand, the old fast-start script enabled prefix caching, and Mamba align raised the page to 1024. The local result was therefore page1024, not the official path’s page832.</p><p>Thus a typical contradiction appeared:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">standalone page832 vendor: very fast</span><br><span class="line">old official submission: almost no gain</span><br></pre></td></tr></table></figure><p>The correct conclusion is not “standalone is untrustworthy”, nor “official is definitely unsuitable”, but to first check:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Does the source code really contain the layout change?</span><br><span class="line">What is the runtime block size?</span><br><span class="line">Does the vendor marker appear?</span><br><span class="line">Did prefix caching change the page?</span><br></pre></td></tr></table></figure><p>Only when these conditions align do performance numbers belong to the same path.</p><h2 id="11-Reproduction-and-Acceptance-Checklist"><a href="#11-Reproduction-and-Acceptance-Checklist" class="headerlink" title="11. Reproduction and Acceptance Checklist"></a>11. Reproduction and Acceptance Checklist</h2><p>If reproducing on another DCU or another vLLM version, it is recommended to follow this order:</p><h3 id="11-1-First-Confirm-Hardware-and-Software-Constraints"><a href="#11-1-First-Confirm-Hardware-and-Software-Constraints" class="headerlink" title="11.1 First Confirm Hardware and Software Constraints"></a>11.1 First Confirm Hardware and Software Constraints</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Confirm gfx936/corresponding DCU target architecture</span><br><span class="line">Confirm wave64-related kernel constraints</span><br><span class="line">Confirm vendor FlashAttention page alignment requirements</span><br><span class="line">Confirm the real model shape of BF16, GQA6, head size 256</span><br></pre></td></tr></table></figure><h3 id="11-2-Then-Confirm-Page-Calculation"><a href="#11-2-Then-Confirm-Page-Calculation" class="headerlink" title="11.2 Then Confirm Page Calculation"></a>11.2 Then Confirm Page Calculation</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Print prefix caching status</span><br><span class="line">Print final cache block size</span><br><span class="line">Print attention page bytes and Mamba page bytes</span><br><span class="line">Confirm page832 comes from alignment calculation, not a hidden parameter in the script</span><br></pre></td></tr></table></figure><h3 id="11-3-Finally-Confirm-Kernel-Hit"><a href="#11-3-Finally-Confirm-Kernel-Hit" class="headerlink" title="11.3 Finally Confirm Kernel Hit"></a>11.3 Finally Confirm Kernel Hit</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Print vendor prefill marker</span><br><span class="line">Print page-contiguous decode marker</span><br><span class="line">Check whether it falls back to unified Triton when conditions are not met</span><br><span class="line">Check that block table and K/V stride are consistent with page size</span><br></pre></td></tr></table></figure><h3 id="11-4-End-to-End-Acceptance"><a href="#11-4-End-to-End-Acceptance" class="headerlink" title="11.4 End-to-End Acceptance"></a>11.4 End-to-End Acceptance</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Same container, same model, same dataset</span><br><span class="line">Sufficient warmup to avoid first-compilation pollution</span><br><span class="line">Three tiers of throughput, failed = 0</span><br><span class="line">Record TTFT, TPOT, output token count, and text</span><br><span class="line">accuracy smoke</span><br></pre></td></tr></table></figure><h2 id="12-Conclusion-The-Real-Optimization-Happens-at-the-Boundary-Between-“Hardware-Constraints”-and-“Framework-Semantics”"><a href="#12-Conclusion-The-Real-Optimization-Happens-at-the-Boundary-Between-“Hardware-Constraints”-and-“Framework-Semantics”" class="headerlink" title="12. Conclusion: The Real Optimization Happens at the Boundary Between “Hardware Constraints” and “Framework Semantics”"></a>12. Conclusion: The Real Optimization Happens at the Boundary Between “Hardware Constraints” and “Framework Semantics”</h2><p>page832 is not an isolated tuning result; it simultaneously involves:</p><ul><li>wave64 execution granularity of gfx936;</li><li>vendor attention’s requirement for 64-token pages;</li><li>page compatibility calculation of vLLM hybrid Attention&#x2F;Mamba;</li><li>block table and stride address formulas of paged KV;</li><li>different access geometries of q&#x3D;4096 prefill and q&#x3D;1 decode;</li><li>numerical validation issues brought by BF16 reduction order.</li></ul><p>If only the kernel is changed without changing page layout, the kernel may not hit at all; if only page size is changed without changing decode access, long-context gains may also be limited; if only microbench is considered without looking at output and official loader, conclusions may also be polluted by wrong paths.</p><p>For DCU inference optimization, the most reusable method is:</p><blockquote><p><strong>First translate the hardware’s access granularity into data layout constraints, then connect the layout constraints into the framework’s existing semantic paths, and finally use runtime markers and end-to-end results to prove the kernel actually hits.</strong></p></blockquote><p>This is closer to real kernel engineering than “change a BLOCK_SIZE and see”, and it better explains why a 48-token page difference repeatedly appears in every layer’s attention and multiple physical page accesses of every long request and affects overall serving performance.</p><hr><p>This article discusses publicly explainable hardware, layout, and kernel methods; the implementation description does not include internal repository addresses, account information, container credentials, or private infrastructure details. Performance numbers come from fixed-shape same-container experiments; actual results will vary with DCU model, driver, DTK, compilation cache, and runtime load.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/07/17/dcu-page832-attention/</id>
    <link href="https://luanjiahao.me/2026/07/17/dcu-page832-attention/"/>
    <published>2026-07-17T04:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Abstract"><a href="#Abstract" class="headerlink" title="Abstract"></a>Abstract</h2><p>In large-model inference, the Attention formula is usually not the hardest part; what really determines performance is often how data lands in memory and at what granularity the hardware can fetch that data]]>
    </summary>
    <title>Hygon DCU Long-Context Attention: From page784 to page832</title>
    <updated>2026-10-28T04:22:26.309Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE/"/>
    <category term="海光 DCU" scheme="https://luanjiahao.me/tags/%E6%B5%B7%E5%85%89-DCU/"/>
    <category term="Qwen3.5" scheme="https://luanjiahao.me/tags/Qwen3-5/"/>
    <category term="vLLM" scheme="https://luanjiahao.me/tags/vLLM/"/>
    <category term="Prefill" scheme="https://luanjiahao.me/tags/Prefill/"/>
    <category term="Decode" scheme="https://luanjiahao.me/tags/Decode/"/>
    <category term="性能分析" scheme="https://luanjiahao.me/tags/%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90/"/>
    <content>
      <![CDATA[<h2 id="Abstract"><a href="#Abstract" class="headerlink" title="Abstract"></a>Abstract</h2><p>In large-model inference optimization, one of the easiest things to misinterpret is treating a local speedup of a certain kernel directly as an improvement in service throughput. Even if a kernel is 1.08x faster in isolated tests, the complete service may be only a few percent faster, or even almost unchanged.</p><p>This article takes Qwen3.5-27B BF16 inference on the Hygon DCU gfx936 path as an example, using profile data, kernel microbenchmarks, and same-container endpoint A&#x2F;B data already completed locally, to answer three questions:</p><ol><li>What operators limit Prefill and Decode respectively;</li><li>Why BF16 M&#x3D;1 GEMM&#x2F;GEMV becomes the main performance wall for Decode;</li><li>What verification a local optimization must go through before it can be judged to have actually improved the complete inference service.</li></ol><p>This article only discusses DCU inference performance engineering. All performance numbers in the article come from existing measurement records in a fixed local environment; standalone kernel data, profile data, and end-to-end service data are labeled separately and are not mixed into a single conclusion.</p><hr><h2 id="1-First-Split-Inference-into-Prefill-and-Decode"><a href="#1-First-Split-Inference-into-Prefill-and-Decode" class="headerlink" title="1. First Split Inference into Prefill and Decode"></a>1. First Split Inference into Prefill and Decode</h2><p>A generation request can usually be divided into two stages:</p><ul><li><strong>Prefill</strong>: processes a longer input at once, converting the prompt into an initial state;</li><li><strong>Decode</strong>: then generates token by token; each step usually processes only one new token while reading previously saved state.</li></ul><p>These two stages look like they belong to the same model, but their hardware behavior is very different.</p><p>Prefill has a larger query dimension, matrix multiplication is closer to a large GEMM, and Attention can also be parallelized over larger token blocks. Decode usually has a query dimension of 1, and many matrix operations degrade into M&#x3D;1 GEMM or GEMV: the amount of computation is not large, but each step must read very large weights and bears a large amount of kernel launch and runtime scheduling overhead.</p><p>A simplified time model can represent the complete request:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">request time = prefill GEMM</span><br><span class="line">             + prefill attention</span><br><span class="line">             + other prefill linear layers</span><br><span class="line">             + decode linear layers</span><br><span class="line">             + decode state access</span><br><span class="line">             + sampling and runtime overhead</span><br></pre></td></tr></table></figure><p>Therefore, “Attention is fast” or “a certain GEMM is fast” only explains one part of the total time. What really deserves attention is: what proportion this part accounts for in the target workload, and whether it recurs in every generated token.</p><hr><h2 id="2-Experimental-Environment-and-Measurement-Protocol"><a href="#2-Experimental-Environment-and-Measurement-Protocol" class="headerlink" title="2. Experimental Environment and Measurement Protocol"></a>2. Experimental Environment and Measurement Protocol</h2><p>The local measurements cited in this article use the following environment and definitions:</p><table><thead><tr><th>Item</th><th>Configuration</th></tr></thead><tbody><tr><td>Accelerator</td><td>Hygon DCU, gfx936-class path</td></tr><tr><td>Model</td><td>Qwen3.5-27B</td></tr><tr><td>Weight compute type</td><td>BF16 original weights</td></tr><tr><td>PyTorch</td><td>2.10.0</td></tr><tr><td>Inference framework</td><td>vLLM local runtime environment</td></tr><tr><td>Standalone warmup</td><td>10 times</td></tr><tr><td>Standalone measured iterations</td><td>40 times</td></tr><tr><td>Endpoint test</td><td>fixed request set, concurrency 1, same-container A&#x2F;B</td></tr><tr><td>Main metrics</td><td>output throughput, mean&#x2F;p99 TPOT, TTFT, output text, failure count</td></tr></tbody></table><p>Measurements are divided into three layers:</p><ol><li><strong>Profile</strong>: answers where time is spent;</li><li><strong>Standalone&#x2F;CUDAGraph microbench</strong>: answers the local upper bound of the target kernel;</li><li><strong>Endpoint A&#x2F;B</strong>: answers how much benefit the complete service actually receives.</li></ol><p>These three layers cannot replace each other. Profile is suitable for locating the problem, not for directly serving as a throughput result; microbench is suitable for screening implementations, not for directly representing the service; only endpoint A&#x2F;B can verify the full-chain effect.</p><p>The profile, RPB2 endpoint, and Tail-M counterexample in this article come from different historical experimental subjects: they are used to explain performance phenomena at different layers, not additive results from the same source combination, and the improvement numbers in this article cannot be added together.</p><hr><h2 id="3-What-a-Long-Context-Profile-Showed"><a href="#3-What-a-Long-Context-Profile-Showed" class="headerlink" title="3. What a Long-Context Profile Showed"></a>3. What a Long-Context Profile Showed</h2><p>The local profile recorded the stage timings of an official 8K prompt-path request with page832, prefix caching disabled, and all-Tail-Q vendor attention:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">q=4096 prefill total: 2720.145 ms</span><br><span class="line">q=1685 prefill total:  464.801 ms</span><br><span class="line">decode total:        2586.460 ms</span><br><span class="line">decode average:        43.108 ms/token</span><br></pre></td></tr></table></figure><h3 id="3-1-Prefill-After-Attention-Large-GEMM-Becomes-the-Main-Time-Bucket"><a href="#3-1-Prefill-After-Attention-Large-GEMM-Becomes-the-Main-Time-Bucket" class="headerlink" title="3.1 Prefill: After Attention, Large GEMM Becomes the Main Time Bucket"></a>3.1 Prefill: After Attention, Large GEMM Becomes the Main Time Bucket</h3><p>In q&#x3D;4096 Prefill, the main time items are as follows:</p><table><thead><tr><th>Time item</th><th align="right">Duration</th></tr></thead><tbody><tr><td>Attention</td><td align="right">238.718 ms</td></tr><tr><td>aten::mm</td><td align="right">1840.596 ms</td></tr><tr><td>aten::mm share of q&#x3D;4096 Prefill</td><td align="right">67.67%</td></tr></tbody></table><p>This set of data gives a very direct judgment: after Attention has already been reduced, continuing to make small adjustments to Attention boundary parameters will have gains limited by its share of total time. At this point, what is more worth studying is the data access, tile organization, and actual call shapes of large GEMM.</p><p>Here aten::mm is not an abstract “model time”, but the aggregate of a batch of real matrix multiplication calls. To continue optimizing, it must be split into specific M, N, K shapes, dtype, call frequency, and whether it is covered by other operators, rather than only looking at the total bucket name.</p><h3 id="3-2-Decode-Linear-GEMV-Takes-the-Vast-Majority-of-Time"><a href="#3-2-Decode-Linear-GEMV-Takes-the-Vast-Majority-of-Time" class="headerlink" title="3.2 Decode: Linear&#x2F;GEMV Takes the Vast Majority of Time"></a>3.2 Decode: Linear&#x2F;GEMV Takes the Vast Majority of Time</h3><p>The Decode statistics from the same profile are as follows:</p><table><thead><tr><th>Time item</th><th align="right">Time per token</th></tr></thead><tbody><tr><td>Linear total</td><td align="right">38.078 ms</td></tr><tr><td>LLMM1</td><td align="right">23.470 ms</td></tr><tr><td>rocBLAS GEMV</td><td align="right">14.607 ms</td></tr><tr><td>Linear share of Decode</td><td align="right">88.33%</td></tr></tbody></table><p>Observed per Decode token:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">LLMM1:        176 launches/token</span><br><span class="line">rocBLAS GEMV: 129 launches/token</span><br></pre></td></tr></table></figure><p>The sources of the 129 GEMV calls can be summarized by module as:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">64  MLP down projection</span><br><span class="line">48  GDN output projection</span><br><span class="line">16  attention output projection</span><br><span class="line">1   lm_head</span><br></pre></td></tr></table></figure><p>This shows that the optimization focus for Decode differs from Prefill. Decode is not simply about finding a “faster Attention”; it must handle a large number of small matrix operations: they are repeated at every step, the saving per call is small, but after multiplying by the number of layers and generated tokens, they may form an appreciable end-to-end difference.</p><hr><h2 id="4-Why-BF16-M-1-Operations-on-DCU-Are-Especially-Difficult"><a href="#4-Why-BF16-M-1-Operations-on-DCU-Are-Especially-Difficult" class="headerlink" title="4. Why BF16 M&#x3D;1 Operations on DCU Are Especially Difficult"></a>4. Why BF16 M&#x3D;1 Operations on DCU Are Especially Difficult</h2><p>When the M dimension of matrix multiplication is 1, compute parallelism is relatively limited, while the weight matrix is still large. In this case, the kernel is often closer to being limited by weight reads and memory access efficiency, rather than by theoretical FLOPS.</p><p>A simplified M&#x3D;1 computation can be written as:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">y[j] = sum_k x[k] * W[j, k]</span><br></pre></td></tr></table></figure><p>Each output row must traverse the K-dimensional weights. Performance is affected jointly by the following factors:</p><ul><li>whether weight loading is contiguous and whether accesses can be coalesced;</li><li>how many output rows a block processes;</li><li>how threads within a wave cooperate during K-dimensional reduction;</li><li>conversion overhead between BF16 reads and FP32 accumulation;</li><li>whether output writeback causes extra synchronization;</li><li>whether kernel launch and graph replay have already become the main cost.</li></ul><p>Therefore, optimization of an M&#x3D;1 kernel is usually not simply increasing the thread count. Too many threads increase reduction and synchronization cost, while too few threads cannot sufficiently hide weight-read latency. Truly effective configurations often only suit a fixed set of model shapes.</p><p>The RPB2 experiment in this article keeps BF16 original weights and retains the verified FP32 pair accumulation, only adjusting the output-row blocking of the same LLMM1 implementation. Here RPB can be understood as rows per block: a block is responsible for a different number of output rows, and the organization of weight reads and reduction changes accordingly.</p><hr><h2 id="5-Local-Standalone-Results-for-BF16-M-1-GEMM"><a href="#5-Local-Standalone-Results-for-BF16-M-1-GEMM" class="headerlink" title="5. Local Standalone Results for BF16 M&#x3D;1 GEMM"></a>5. Local Standalone Results for BF16 M&#x3D;1 GEMM</h2><p>On the same DCU, with the same BF16 inputs and the same compiled extension, RPB4 and RPB2 are compared:</p><table><thead><tr><th>Weight shape</th><th align="right">RPB4</th><th align="right">RPB2</th><th align="right">Speedup</th><th align="right">Max difference</th></tr></thead><tbody><tr><td>14336 x 5120</td><td align="right">0.114160 ms</td><td align="right">0.106480 ms</td><td align="right">1.0721x</td><td align="right">0</td></tr><tr><td>16384 x 5120</td><td align="right">0.131040 ms</td><td align="right">0.120960 ms</td><td align="right">1.0833x</td><td align="right">0</td></tr><tr><td>34816 x 5120</td><td align="right">0.272480 ms</td><td align="right">0.250400 ms</td><td align="right">1.0882x</td><td align="right">0</td></tr></tbody></table><p>The test conditions were warmup 10 times and 40 formal measurement iterations. The outputs of all three shapes are bitwise identical to the reference path, and repeated runs are stable. Estimated from the weight matrix bytes divided by the measured kernel time, the effective weight bandwidth is about 1.38-1.42 TB&#x2F;s; this is an effective bandwidth estimate for this kernel, not a direct measurement of DCU peak HBM bandwidth.</p><p>This set of data shows that the RPB2 gain is a real local execution gain, but it has not yet answered the service-layer question. Standalone only measured the target matrix multiplication; complete Decode also includes other Linear operations, state reads, sampling, and runtime overhead.</p><hr><h2 id="6-From-Kernel-to-Service-Same-Container-A-B-Is-the-Key-Gate"><a href="#6-From-Kernel-to-Service-Same-Container-A-B-Is-the-Key-Gate" class="headerlink" title="6. From Kernel to Service: Same-Container A&#x2F;B Is the Key Gate"></a>6. From Kernel to Service: Same-Container A&#x2F;B Is the Key Gate</h2><p>To determine whether the local gain can propagate to complete inference, A&#x2F;B was performed locally in the same container, with the same model, the same request set, and the same runtime conditions. Both the baseline configuration and RPB2 use BF16 original weights. The endpoint metrics are as follows:</p><table><thead><tr><th>Tier</th><th align="right">Baseline output tok&#x2F;s</th><th align="right">RPB2 output tok&#x2F;s</th><th align="right">Throughput change</th><th align="right">Baseline p99 TPOT</th><th align="right">RPB2 p99 TPOT</th></tr></thead><tbody><tr><td>4K-8K</td><td align="right">18.8369</td><td align="right">19.5961</td><td align="right">+4.03%</td><td align="right">46.9627 ms</td><td align="right">44.9923 ms</td></tr><tr><td>8K-16K</td><td align="right">14.5837</td><td align="right">15.0306</td><td align="right">+3.06%</td><td align="right">47.5920 ms</td><td align="right">45.5662 ms</td></tr><tr><td>16K-32K</td><td align="right">11.7495</td><td align="right">12.0286</td><td align="right">+2.38%</td><td align="right">49.8507 ms</td><td align="right">47.8545 ms</td></tr></tbody></table><p>The relationship between endpoint results and standalone is typical: the local kernel is 1.07x-1.09x, while the service layer does not show the same 7%-9% change, but rather 2.38%-4.03%. The reason is that RPB2 only affects part of the repeatedly executed Linear work, and the remaining time is still composed of other operators and runtime.</p><p>The correctness and stability checks are as follows:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">4K-8K:   2644 vs 2644 output tokens, 10/10 text byte-identical</span><br><span class="line">8K-16K:  1768 vs 1768 output tokens, 10/10 text byte-identical</span><br><span class="line">16K-32K: 1877 vs 1877 output tokens, 10/10 text byte-identical</span><br><span class="line">failed:  0</span><br></pre></td></tr></table></figure><p>TTFT is basically unchanged:</p><table><thead><tr><th>Tier</th><th align="right">Baseline p99 TTFT</th><th align="right">RPB2 p99 TTFT</th></tr></thead><tbody><tr><td>4K-8K</td><td align="right">2049.317 ms</td><td align="right">2047.251 ms</td></tr><tr><td>8K-16K</td><td align="right">4620.050 ms</td><td align="right">4614.045 ms</td></tr><tr><td>16K-32K</td><td align="right">7044.241 ms</td><td align="right">7037.622 ms</td></tr></tbody></table><p>This matches the expectation for the optimization location: RPB2 mainly acts on Decode M&#x3D;1 Linear, and the Prefill path for the first token has no obvious change, so TTFT is basically unchanged while TPOT improves.</p><hr><h2 id="7-Why-a-1-08x-Kernel-Does-Not-Automatically-Become-a-1-08x-Service"><a href="#7-Why-a-1-08x-Kernel-Does-Not-Automatically-Become-a-1-08x-Service" class="headerlink" title="7. Why a 1.08x Kernel Does Not Automatically Become a 1.08x Service"></a>7. Why a 1.08x Kernel Does Not Automatically Become a 1.08x Service</h2><p>This phenomenon can be understood with a rough Amdahl model. Suppose the target kernel accounts for a proportion f of total time and the local speedup is s. Under ideal conditions, the overall speedup upper bound is approximately:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">service speedup ≈ 1 / ((1 - f) + f / s)</span><br></pre></td></tr></table></figure><p>When s &#x3D; 1.08, if the target kernel covers only part of the total time, the overall gain is necessarily less than 8%. The actual service is also affected by more factors:</p><ul><li>the matrix shapes of different layers are not exactly the same;</li><li>some calls still use the original implementation;</li><li>kernel launch, graph replay, and state access cannot be eliminated by the target kernel;</li><li>the large GEMM in Prefill and the small GEMM in Decode are located in different time phases;</li><li>measurements also contain fluctuations caused by request length, cache, and system scheduling.</li></ul><p>Therefore, the correct question for end-to-end A&#x2F;B is not “how many times faster is this kernel”, but:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">How many real calls does it cover?</span><br><span class="line">What share of total time do these calls account for?</span><br><span class="line">Does it change the behavior of other paths?</span><br></pre></td></tr></table></figure><p>The reason the local RPB2 results are meaningful is precisely that it simultaneously satisfies three points: the target calls appear many times in Decode, the local gain repeats across multiple real shapes, and the output and request behavior remain consistent.</p><hr><h2 id="8-A-Counterexample-Standalone-Gain-Endpoint-Almost-Unchanged"><a href="#8-A-Counterexample-Standalone-Gain-Endpoint-Almost-Unchanged" class="headerlink" title="8. A Counterexample: Standalone Gain, Endpoint Almost Unchanged"></a>8. A Counterexample: Standalone Gain, Endpoint Almost Unchanged</h2><p>The same batch of local experiments also provided a very representative counterexample. After screening 145 shapes and M combinations across five types of large GEMM, 106 shapes simultaneously satisfied:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">BF16 output hash consistent with default path</span><br><span class="line">Standalone CUDAGraph speedup &gt;= 1.01x</span><br></pre></td></tr></table></figure><p>Looking at these shapes alone, they all look like optimizations worth integrating. After integrating them into the complete service, however, the result for the fixed mid-tier request set was only:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">output throughput: 16.583027 -&gt; 16.586315 tok/s</span><br><span class="line">increment:         +0.0198%</span><br><span class="line">texts:             10/10 exact</span><br><span class="line">tokens:            1809 -&gt; 1809</span><br><span class="line">failed:            0 -&gt; 0</span><br></pre></td></tr></table></figure><p>Another gate&#x2F;up GEMM scheme that preserves exact output also achieved an average gain of only +0.3250% in steady-state same-container A&#x2F;B. This does not mean the standalone measurements were wrong; it means that the time share, call coverage, or overlapability of these calls in the complete request is insufficient to form an obvious service gain.</p><p>This counterexample is very important:</p><blockquote><p>Standalone speedup is a local upper bound; endpoint A&#x2F;B is the product-layer gain.</p></blockquote><p>If schemes are selected only based on microbench, it is easy to stack dozens of small positive gains into a seemingly large prediction, but at runtime they may overlap each other, be drowned out by other time items, or hit only very few real requests.</p><hr><h2 id="9-How-Correctness-and-Stability-Should-Be-Verified"><a href="#9-How-Correctness-and-Stability-Should-Be-Verified" class="headerlink" title="9. How Correctness and Stability Should Be Verified"></a>9. How Correctness and Stability Should Be Verified</h2><p>Performance optimization cannot just record a duration number. For BF16 kernels on DCU, at least the following should be checked simultaneously.</p><h3 id="9-1-Kernel-Layer"><a href="#9-1-Kernel-Layer" class="headerlink" title="9.1 Kernel Layer"></a>9.1 Kernel Layer</h3><ul><li>whether the output is finite;</li><li>whether the output shape and stride are correct;</li><li>the maximum absolute difference or hash compared with the reference path;</li><li>whether repeated runs are stable;</li><li>whether it is still correct for different batches, different M values, and boundary shapes.</li></ul><h3 id="9-2-Service-Layer"><a href="#9-2-Service-Layer" class="headerlink" title="9.2 Service Layer"></a>9.2 Service Layer</h3><ul><li>whether each request completes;</li><li>whether the output token count is consistent;</li><li>whether the generated text is byte-identical, or meets a preset accuracy standard;</li><li>whether mean&#x2F;p99 TPOT improves;</li><li>whether TTFT regresses;</li><li>whether there are failed requests, timeouts, or abnormal retries.</li></ul><h3 id="9-3-Measurement-Layer"><a href="#9-3-Measurement-Layer" class="headerlink" title="9.3 Measurement Layer"></a>9.3 Measurement Layer</h3><ul><li>whether the two configurations use the same model and the same extension;</li><li>whether they are compared in the same container and with the same request set;</li><li>whether compilation, first load, and cache setup times are handled separately;</li><li>whether sufficient warmup is used;</li><li>whether repeated runs confirm the direction, rather than trusting a single result.</li></ul><p>In particular, avoid treating “the code path exists” as “the kernel has been hit”. The most reliable approach is to record the real dispatch, input shapes, and selected implementation at runtime, then correspond the markers with performance results one-to-one.</p><hr><h2 id="10-A-Reusable-DCU-Inference-Optimization-Workflow"><a href="#10-A-Reusable-DCU-Inference-Optimization-Workflow" class="headerlink" title="10. A Reusable DCU Inference Optimization Workflow"></a>10. A Reusable DCU Inference Optimization Workflow</h2><h3 id="Step-1-First-Distinguish-Prefill-and-Decode"><a href="#Step-1-First-Distinguish-Prefill-and-Decode" class="headerlink" title="Step 1: First Distinguish Prefill and Decode"></a>Step 1: First Distinguish Prefill and Decode</h3><p>Measure TTFT, TPOT, and stage timings separately. Do not use Decode optimization methods to explain Prefill, and do not use conclusions about Prefill large GEMM to replace Decode M&#x3D;1 analysis.</p><h3 id="Step-2-Use-Profile-to-Find-the-Time-Wall"><a href="#Step-2-Use-Profile-to-Find-the-Time-Wall" class="headerlink" title="Step 2: Use Profile to Find the Time Wall"></a>Step 2: Use Profile to Find the Time Wall</h3><p>First confirm whether the cost is large GEMM, GEMV, Attention, state reads&#x2F;writes, or runtime overhead. Only by knowing the time share can you know whether a local speedup has enough physical budget.</p><h3 id="Step-3-Restore-the-Real-Call-Shapes"><a href="#Step-3-Restore-the-Real-Call-Shapes" class="headerlink" title="Step 3: Restore the Real Call Shapes"></a>Step 3: Restore the Real Call Shapes</h3><p>Record M, N, K, dtype, batch, call count, and output writeback method. Microbenchmarks on generalized matrices can only serve as references and cannot replace real shapes.</p><h3 id="Step-4-Screen-in-Standalone"><a href="#Step-4-Screen-in-Standalone" class="headerlink" title="Step 4: Screen in Standalone"></a>Step 4: Screen in Standalone</h3><p>First use fixed warmup and multiple iterations to measure local time, while checking correctness and determinism. For implementations clearly below the threshold, do not rush to integrate them into the service.</p><h3 id="Step-5-Do-Same-Container-Endpoint-A-B"><a href="#Step-5-Do-Same-Container-Endpoint-A-B" class="headerlink" title="Step 5: Do Same-Container Endpoint A&#x2F;B"></a>Step 5: Do Same-Container Endpoint A&#x2F;B</h3><p>Keep the model, dataset, concurrency, sampling settings, and runtime environment consistent, and record throughput, TPOT, TTFT, output length, and failure count separately. A candidate must repeatedly show a positive signal across multiple tiers or multiple real shapes.</p><h3 id="Step-6-Keep-Negative-Results"><a href="#Step-6-Keep-Negative-Results" class="headerlink" title="Step 6: Keep Negative Results"></a>Step 6: Keep Negative Results</h3><p>A scheme that is positive in standalone but has no endpoint gain is also a valuable engineering conclusion. It can help subsequent work avoid repeated testing and bring the performance budget closer to the real service instead of stopping at the kernel table.</p><hr><h2 id="11-Conclusion"><a href="#11-Conclusion" class="headerlink" title="11. Conclusion"></a>11. Conclusion</h2><p>The local measured data presents a relatively clear DCU inference performance chain:</p><ol><li>The main contradiction in Prefill can fall on large GEMM; in the q&#x3D;4096 profile, aten::mm accounts for 67.67%;</li><li>The main contradiction in Decode is a large number of Linear&#x2F;GEMV operations; Linear accounts for 88.33% of Decode in that profile;</li><li>A small local improvement in the BF16 M&#x3D;1 kernel can propagate to the endpoint after repeated calls; RPB2 brings 2.38%-4.03% throughput improvement across three same-container A&#x2F;B tiers;</li><li>Local speedup does not guarantee end-to-end gain; 106 standalone-positive and exact tail large GEMM shapes only produced +0.0198% after integration;</li><li>Correctness, dispatch hit, repeatability, and measurement protocol are as important as the kernel’s own time.</li></ol><p>For inference optimization on Hygon DCU, the most reusable thing is not a fixed block parameter, but a verification order:</p><blockquote><p>First use profile to find the real time wall, then use real shapes for kernel microbench, and finally use same-container endpoint A&#x2F;B to verify whether it improves the complete service.</p></blockquote><p>Only when the three layers of evidence point in the same direction does local “fast” deserve to be called service-layer “fast”.</p><hr><p>The performance numbers in this article come from existing measurement records in a fixed local environment and are only used to illustrate engineering observations under this model, software stack, and DCU path. Different DCU models, drivers, DTK, compilation caches, request distributions, and system loads may produce different results; standalone kernel speedups should not be directly extrapolated as general service throughput.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/07/17/dcu-long-context-inference-measurement/</id>
    <link href="https://luanjiahao.me/2026/07/17/dcu-long-context-inference-measurement/"/>
    <published>2026-07-17T03:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Abstract"><a href="#Abstract" class="headerlink" title="Abstract"></a>Abstract</h2><p>In large-model inference optimization, one of the easiest things to misinterpret is treating a local speedup of a certain kernel directly as an improvement in service throughput. Even if a kernel is 1.08x f]]>
    </summary>
    <title>Qwen3.5 Inference Performance Engineering on Hygon DCU: From Prefill/Decode Hotspots to End-to-End Gains</title>
    <updated>2026-10-28T04:22:26.309Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE/"/>
    <category term="海光 DCU" scheme="https://luanjiahao.me/tags/%E6%B5%B7%E5%85%89-DCU/"/>
    <category term="vLLM" scheme="https://luanjiahao.me/tags/vLLM/"/>
    <category term="性能分析" scheme="https://luanjiahao.me/tags/%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90/"/>
    <category term="Profile" scheme="https://luanjiahao.me/tags/Profile/"/>
    <category term="GPU Kernel" scheme="https://luanjiahao.me/tags/GPU-Kernel/"/>
    <content>
      <![CDATA[<h2 id="Opening"><a href="#Opening" class="headerlink" title="Opening"></a>Opening</h2><p>When profiling large models on Hygon DCU, the hardest part is usually not whether a trace can be generated, but how to read it after it has been generated.</p><p>A local sampling run of Qwen3.5-27B BF16 produced:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">总事件:       235,964</span><br><span class="line">GPU 事件:      61,804</span><br><span class="line">kernel 事件:   60,124</span><br><span class="line">GPU event 总时长: 5773.493 ms</span><br></pre></td></tr></table></figure><p>Sorting directly by total kernel time only shows a long list of Tensile, Triton, Attention, and custom kernel names. It does not proactively tell you:</p><ul><li>which ones belong to Prefill;</li><li>which ones belong to Decode;</li><li>how many times a kernel is called per token;</li><li>whether the call counts can map back to the number of model layers;</li><li>which time bucket is the real bottleneck worth optimizing.</li></ul><p>This article does not discuss a new kernel. Instead, using this measured Hygon DCU trace, it demonstrates how to reconstruct the complete inference structure from event context and call counts.</p><hr><h2 id="1-First-Limit-the-Scope-of-the-Profile"><a href="#1-First-Limit-the-Scope-of-the-Profile" class="headerlink" title="1. First, Limit the Scope of the Profile"></a>1. First, Limit the Scope of the Profile</h2><p>This sampling covers only one request with clear boundaries:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">3 个 q=4096 Prefill chunk</span><br><span class="line">1 个 q=1685 Prefill tail</span><br><span class="line">60 个 Decode step</span><br></pre></td></tr></table></figure><p>This is a very important precondition.</p><p>If the profile covers multiple concurrent requests, model loading, first compilation, and long generation, the number of events will expand rapidly, and it will be difficult to attribute a given kernel to a specific phase.</p><p>For Hygon DCU large-model services, a more reliable workflow is:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">服务先完成加载和必要 warmup</span><br><span class="line">-&gt; 开启 profiler</span><br><span class="line">-&gt; 只发送一个有代表性的请求</span><br><span class="line">-&gt; 等请求完整结束</span><br><span class="line">-&gt; 立即停止并保存 trace</span><br></pre></td></tr></table></figure><p>The goal of profiling is to answer “where is the time spent,” not to reproduce maximum throughput.</p><hr><h2 id="2-First-Cut-Split-Prefill-and-Decode-by-Execution-Context"><a href="#2-First-Cut-Split-Prefill-and-Decode-by-Execution-Context" class="headerlink" title="2. First Cut: Split Prefill and Decode by Execution Context"></a>2. First Cut: Split Prefill and Decode by Execution Context</h2><p>Each batch of GPU events in the trace has an upper-level execution context. Using the token count and generation state, they can first be divided into:</p><table><thead><tr><th>Context</th><th align="right">Total GPU event time</th></tr></thead><tbody><tr><td>Three q&#x3D;4096 Prefill chunks</td><td align="right">2720.145 ms</td></tr><tr><td>One q&#x3D;1685 Prefill tail</td><td align="right">464.801 ms</td></tr><tr><td>60 Decode steps</td><td align="right">2586.460 ms</td></tr></tbody></table><p>Decode average per step:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">2586.460 ms / 60</span><br><span class="line">= 43.108 ms/token</span><br></pre></td></tr></table></figure><p>This step is more important than looking directly at the top kernels.</p><p>The same GEMM name may appear in both Prefill and Decode, but the M dimension, parallelism, and optimization methods are completely different in the two phases. Only after splitting by context do the later statistics have physical meaning.</p><hr><h2 id="3-Second-Cut-Identify-Model-Structure-Using-Call-Counts"><a href="#3-Second-Cut-Identify-Model-Structure-Using-Call-Counts" class="headerlink" title="3. Second Cut: Identify Model Structure Using Call Counts"></a>3. Second Cut: Identify Model Structure Using Call Counts</h2><h3 id="3-1-LLMM1-kernel"><a href="#3-1-LLMM1-kernel" class="headerlink" title="3.1 LLMM1 kernel"></a>3.1 LLMM1 kernel</h3><p>In the Decode interval, LLMM1 totals:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">10,560 launches</span><br><span class="line">1,408.219 ms</span><br></pre></td></tr></table></figure><p>Divided by 60 Decode steps:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">10,560 / 60 = 176 launches/token</span><br><span class="line">1,408.219 / 60 = 23.470 ms/token</span><br></pre></td></tr></table></figure><p>176 is not a random number; it maps completely back to the model structure:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">64 次 MLP gate/up</span><br><span class="line">48 次 GDN qkvz</span><br><span class="line">48 次 GDN BA</span><br><span class="line">16 次 Full-Attention QKV</span><br><span class="line">-------------------------</span><br><span class="line">176 次/token</span><br></pre></td></tr></table></figure><p>When the trace call count and the model layer count fully close, the kernel attribution is basically credible.</p><h3 id="3-2-rocBLAS-Tensile-GEMV"><a href="#3-2-rocBLAS-Tensile-GEMV" class="headerlink" title="3.2 rocBLAS&#x2F;Tensile GEMV"></a>3.2 rocBLAS&#x2F;Tensile GEMV</h3><p>In the same Decode interval, rocBLAS&#x2F;Tensile GEMV is:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">7,740 launches</span><br><span class="line">876.431 ms</span><br></pre></td></tr></table></figure><p>After conversion:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">7,740 / 60 = 129 launches/token</span><br><span class="line">876.431 / 60 = 14.607 ms/token</span><br></pre></td></tr></table></figure><p>129 can likewise be mapped back to modules:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">64 次 MLP down projection</span><br><span class="line">48 次 GDN output projection</span><br><span class="line">16 次 Attention output projection</span><br><span class="line"> 1 次 lm_head</span><br><span class="line">--------------------------------</span><br><span class="line">129 次/token</span><br></pre></td></tr></table></figure><p>This kind of “call-count closure” is very valuable. It not only tells us whether a kernel is hot, but also checks:</p><ul><li>whether some type of Linear was missed in the statistics;</li><li>whether a fusion actually reduced launches;</li><li>whether runtime markers are consistent with the actual call count;</li><li>whether the model structure and the trace come from the same version.</li></ul><hr><h2 id="4-Third-Cut-Convert-Time-into-a-Per-Token-Budget"><a href="#4-Third-Cut-Convert-Time-into-a-Per-Token-Budget" class="headerlink" title="4. Third Cut: Convert Time into a Per-Token Budget"></a>4. Third Cut: Convert Time into a Per-Token Budget</h2><p>LLMM1 and rocBLAS combined:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">23.470 + 14.607</span><br><span class="line">= 38.078 ms/token</span><br></pre></td></tr></table></figure><p>Relative to 43.108 ms&#x2F;token for Decode:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">38.078 / 43.108</span><br><span class="line">= 88.33%</span><br></pre></td></tr></table></figure><p>This shows that in this Hygon DCU trace, the main Decode bottleneck is Linear, not some scattered pointwise kernel.</p><p>Other major Decode time includes:</p><table><thead><tr><th>Module</th><th align="right">Time per token</th><th align="right">Decode share</th></tr></thead><tbody><tr><td>Page-contiguous Attention</td><td align="right">1.630 ms</td><td align="right">3.78%</td></tr><tr><td>Packed GDN recurrent</td><td align="right">0.623 ms</td><td align="right">1.44%</td></tr></tbody></table><p>After seeing this ratio, optimization priorities become clear:</p><ul><li>a small fusion that saves only 2-3 us per layer is unlikely to change total TPOT;</li><li>an implementation covering hundreds of Linear calls may accumulate endpoint gains even if each call is only a few percent faster;</li><li>continuing to optimize a bucket that accounts for less than 2% requires first proving a sufficiently large absolute saving.</li></ul><p>The role of profiling is not to automatically provide a solution, but to constrain the physical upper bound of the solution.</p><hr><h2 id="5-Prefill-Cannot-Reuse-Decode’s-Conclusions"><a href="#5-Prefill-Cannot-Reuse-Decode’s-Conclusions" class="headerlink" title="5. Prefill Cannot Reuse Decode’s Conclusions"></a>5. Prefill Cannot Reuse Decode’s Conclusions</h2><p>The main time items for the three q&#x3D;4096 Prefill chunks are:</p><table><thead><tr><th>Time item</th><th align="right">Total time</th><th align="right">Share of q&#x3D;4096 Prefill</th></tr></thead><tbody><tr><td>aten::mm</td><td align="right">1840.596 ms</td><td align="right">67.67%</td></tr><tr><td>Attention</td><td align="right">238.718 ms</td><td align="right">8.78%</td></tr></tbody></table><p>The same model presents different shapes in Prefill and Decode:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Prefill:</span><br><span class="line">  大 M GEMM + 长序列 Attention</span><br><span class="line"></span><br><span class="line">Decode:</span><br><span class="line">  M=1 Linear/GEMV + 状态读取</span><br></pre></td></tr></table></figure><p>Therefore, in trace analysis, “Linear accounts for 88.33% of Decode” cannot be understood as a unified share for the entire request, nor can Decode’s M&#x3D;1 kernels be used to explain q&#x3D;4096 Prefill.</p><p>The correct approach is to establish two separate budgets:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">TTFT 预算 -&gt; Prefill 上下文</span><br><span class="line">TPOT 预算 -&gt; Decode 上下文</span><br></pre></td></tr></table></figure><hr><h2 id="6-Why-Looking-Only-at-Kernel-Rankings-Misleads"><a href="#6-Why-Looking-Only-at-Kernel-Rankings-Misleads" class="headerlink" title="6. Why Looking Only at Kernel Rankings Misleads"></a>6. Why Looking Only at Kernel Rankings Misleads</h2><p>Suppose the first place in the top kernel list is LLMM1 and the second place is some Tensile kernel.</p><p>Without context splitting, you will encounter at least three problems.</p><h3 id="6-1-The-Same-Name-May-Correspond-to-Different-Shapes"><a href="#6-1-The-Same-Name-May-Correspond-to-Different-Shapes" class="headerlink" title="6.1 The Same Name May Correspond to Different Shapes"></a>6.1 The Same Name May Correspond to Different Shapes</h3><p>The same library kernel may be reused by multiple Linear modules. You need to combine M, N, K, call count, and parent events to map it to a model layer.</p><h3 id="6-2-High-Total-Time-May-Just-Mean-Many-Calls"><a href="#6-2-High-Total-Time-May-Just-Mean-Many-Calls" class="headerlink" title="6.2 High Total Time May Just Mean Many Calls"></a>6.2 High Total Time May Just Mean Many Calls</h3><p>A 130 us kernel called ten thousand times will rank ahead of a 5 ms kernel. When optimizing, look at all of:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">单次耗时</span><br><span class="line">调用次数</span><br><span class="line">覆盖层数</span><br><span class="line">端点时间占比</span><br></pre></td></tr></table></figure><h3 id="6-3-The-Profile-Itself-Is-Not-an-Endpoint-Benchmark"><a href="#6-3-The-Profile-Itself-Is-Not-an-Endpoint-Benchmark" class="headerlink" title="6.3 The Profile Itself Is Not an Endpoint Benchmark"></a>6.3 The Profile Itself Is Not an Endpoint Benchmark</h3><p>The profiler introduces recording, synchronization, and buffering overhead. The 43.108 ms&#x2F;token in the trace is used to establish the time composition and cannot directly replace the service TPOT without a profiler.</p><hr><h2 id="7-A-Real-Profiler-Shutdown-Failure"><a href="#7-A-Real-Profiler-Shutdown-Failure" class="headerlink" title="7. A Real Profiler Shutdown Failure"></a>7. A Real Profiler Shutdown Failure</h2><p>The target request completed normally, and the trace file was successfully written; but after stop_profile was called, the ROCm runtime triggered a VMFault, and EngineCore subsequently exited.</p><p>Therefore, this run was handled as follows:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">已写出的 trace:</span><br><span class="line">  作为诊断证据保留</span><br><span class="line"></span><br><span class="line">profiled request 的吞吐和 TPOT:</span><br><span class="line">  不作为端点性能证据</span><br><span class="line"></span><br><span class="line">发生故障后的服务:</span><br><span class="line">  不继续复用做 benchmark</span><br></pre></td></tr></table></figure><p>This boundary is very important.</p><p>“A service crash when stopping the profiler” does not mean that all previously written events are invalid; but it does mean that the profiler lifecycle has changed the service state, and the same process cannot continue to be used for performance A&#x2F;B.</p><p>When collecting long traces on Hygon DCU, it is recommended to treat the profiler service as a one-time diagnostic instance.</p><hr><h2 id="8-A-Reusable-Hygon-DCU-Trace-Analysis-Workflow"><a href="#8-A-Reusable-Hygon-DCU-Trace-Analysis-Workflow" class="headerlink" title="8. A Reusable Hygon DCU Trace Analysis Workflow"></a>8. A Reusable Hygon DCU Trace Analysis Workflow</h2><h3 id="Step-1-Fix-the-Request"><a href="#Step-1-Fix-the-Request" class="headerlink" title="Step 1: Fix the Request"></a>Step 1: Fix the Request</h3><p>Choose a single request that can cover the target Prefill shape and enough Decode steps.</p><h3 id="Step-2-Record-the-Coverage-Scope"><a href="#Step-2-Record-the-Coverage-Scope" class="headerlink" title="Step 2: Record the Coverage Scope"></a>Step 2: Record the Coverage Scope</h3><p>Explicitly write down:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Prefill chunk 数量与 token 数</span><br><span class="line">Decode step 数量</span><br><span class="line">是否包含首次编译</span><br><span class="line">是否包含模型加载</span><br></pre></td></tr></table></figure><h3 id="Step-3-Bucket-by-Context"><a href="#Step-3-Bucket-by-Context" class="headerlink" title="Step 3: Bucket by Context"></a>Step 3: Bucket by Context</h3><p>At minimum, divide into:</p><ul><li>large Prefill;</li><li>tail Prefill;</li><li>Decode;</li><li>sampling and copies outside the execute context.</li></ul><h3 id="Step-4-Count-Kernel-Calls-and-Total-Time"><a href="#Step-4-Count-Kernel-Calls-and-Total-Time" class="headerlink" title="Step 4: Count Kernel Calls and Total Time"></a>Step 4: Count Kernel Calls and Total Time</h3><p>Do not keep only the top-N times; also keep the count and average time.</p><h3 id="Step-5-Divide-by-Decode-Steps"><a href="#Step-5-Divide-by-Decode-Steps" class="headerlink" title="Step 5: Divide by Decode Steps"></a>Step 5: Divide by Decode Steps</h3><p>Convert the total call count and total time into:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">launches/token</span><br><span class="line">ms/token</span><br></pre></td></tr></table></figure><h3 id="Step-6-Map-Back-to-Model-Layers"><a href="#Step-6-Map-Back-to-Model-Layers" class="headerlink" title="Step 6: Map Back to Model Layers"></a>Step 6: Map Back to Model Layers</h3><p>Check whether the call count can be fully summed from the MLP, GDN, Attention, and lm_head layer counts.</p><h3 id="Step-7-Calculate-the-Physical-Upper-Bound"><a href="#Step-7-Calculate-the-Physical-Upper-Bound" class="headerlink" title="Step 7: Calculate the Physical Upper Bound"></a>Step 7: Calculate the Physical Upper Bound</h3><p>Estimate for candidate optimizations:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">maximum endpoint saving</span><br><span class="line">= current bucket time</span><br><span class="line">  × removable fraction</span><br></pre></td></tr></table></figure><h3 id="Step-8-Start-a-Clean-Service-for-A-B"><a href="#Step-8-Start-a-Clean-Service-for-A-B" class="headerlink" title="Step 8: Start a Clean Service for A&#x2F;B"></a>Step 8: Start a Clean Service for A&#x2F;B</h3><p>Profiling is responsible for locating; the endpoint in the same environment without a profiler is responsible for confirming real throughput, TTFT, TPOT, and output consistency.</p><hr><h2 id="9-What-This-Means-for-Hygon-DCU-Developers"><a href="#9-What-This-Means-for-Hygon-DCU-Developers" class="headerlink" title="9. What This Means for Hygon DCU Developers"></a>9. What This Means for Hygon DCU Developers</h2><p>This trace proves that in the gfx936 local software stack, the PyTorch&#x2F;ROCm-compatible trace path can simultaneously observe:</p><ul><li>Tensile GEMM;</li><li>custom HIP kernels;</li><li>Triton kernels;</li><li>CUDAGraph replay;</li><li>Attention and GDN paths;</li><li>Prefill and Decode contexts.</li></ul><p>But the data volume is large. What is truly useful is not generating a trace file, but compressing 60,124 kernel events into a verifiable model structure:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">176 次 LLMM1/token</span><br><span class="line">129 次 rocBLAS GEMV/token</span><br><span class="line">38.078 ms Linear/token</span><br><span class="line">88.33% Decode 占比</span><br></pre></td></tr></table></figure><p>Only when the call count, layer count, and time all close does the profile turn from a “pretty timeline” into engineering evidence that can guide Hygon DCU optimization.</p><hr><h2 id="10-Conclusion"><a href="#10-Conclusion" class="headerlink" title="10. Conclusion"></a>10. Conclusion</h2><p>From 235,964 trace events, the local reconstruction yielded:</p><ul><li>three q&#x3D;4096 Prefill chunks;</li><li>one q&#x3D;1685 tail;</li><li>60 Decode steps;</li><li>176 LLMM1 calls per token;</li><li>129 rocBLAS GEMV calls per token;</li><li>Linear totaling 38.078 ms&#x2F;token;</li><li>Linear accounting for 88.33% of Decode.</li></ul><p>The core of this article is not a kernel ranking, but a Hygon DCU profiling method:</p><blockquote><p>First split phases by execution context, then reconstruct the model structure using call counts, and finally convert total time into a per-token physical budget.</p></blockquote><p>Profiling is used to decide where it is worth doing work; endpoint A&#x2F;B is used to decide whether the work is actually effective. The two kinds of evidence cannot be mixed.</p><hr><p>The data in this article comes from a local gfx936 diagnostic trace with a clear coverage scope. Profiler execution has additional overhead; the profile times in this article are only used for operator attribution and budget analysis and do not represent the final throughput of a service without a profiler.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/07/17/dcu-profile-kernel-to-token/</id>
    <link href="https://luanjiahao.me/2026/07/17/dcu-profile-kernel-to-token/"/>
    <published>2026-07-17T02:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Opening"><a href="#Opening" class="headerlink" title="Opening"></a>Opening</h2><p>When profiling large models on Hygon DCU, the hardest part is usually not whether a trace can be generated, but how to read it after it has been generated.</p>
<p>A local sampling run of Qwen3.5-27B BF16 produc]]>
    </summary>
    <title>Hygon DCU Profile in Practice: How to Reconstruct a Token from Sixty Thousand GPU Kernels</title>
    <updated>2026-10-28T04:22:26.321Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE/"/>
    <category term="海光 DCU" scheme="https://luanjiahao.me/tags/%E6%B5%B7%E5%85%89-DCU/"/>
    <category term="vLLM" scheme="https://luanjiahao.me/tags/vLLM/"/>
    <category term="ROCm" scheme="https://luanjiahao.me/tags/ROCm/"/>
    <category term="Kernel" scheme="https://luanjiahao.me/tags/Kernel/"/>
    <category term="性能工程" scheme="https://luanjiahao.me/tags/%E6%80%A7%E8%83%BD%E5%B7%A5%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>The biggest waste of time I encountered while modifying vLLM kernels on Hygon DCU was not a compilation error.</p><p>That time, compilation ended normally, the service could start, the endpoint could return results, and the performance test even produced a change that looked explainable. Only after rechecking the runtime environment did I discover a more troublesome problem: the source code, build artifacts, and the files actually loaded by the service were not in the same state. All previous judgments about performance had to be discarded.</p><p>This changed my debugging order. Now every time I finish modifying a ROCm kernel, I do not immediately look at throughput; instead, I first answer a more basic question:</p><blockquote><p>Is what is actually executing on the Hygon DCU really the code I just modified, compiled, and was about to test?</p></blockquote><p>This article records a set of vLLM incremental compilation and runtime verification methods that I have actually used on a Hygon DCU with the gfx936 architecture. The focus is not introducing some faster operator, but rather opening up one by one the links between <code>.cu</code> source and real kernel dispatch where stale state can easily hide.</p><h2 id="Experimental-Environment"><a href="#Experimental-Environment" class="headerlink" title="Experimental Environment"></a>Experimental Environment</h2><p>The measured environment involved in this article is as follows. The timings and binary information later are only valid for this environment and should not be extrapolated as fixed results for other DCU, DTK, or vLLM versions.</p><table><thead><tr><th>Item</th><th>Measured environment</th></tr></thead><tbody><tr><td>Accelerator card</td><td>Hygon DCU, gfx936 architecture, device name shown as <code>BW</code></td></tr><tr><td>DTK</td><td>DTK 26.04 series environment</td></tr><tr><td>Python</td><td>3.10</td></tr><tr><td>PyTorch</td><td>2.10.0</td></tr><tr><td>Build tools</td><td>CMake, Ninja, hipify, ccache 4.5.1</td></tr><tr><td>vLLM ROCm extension</td><td><code>vllm._rocm_C</code></td></tr></tbody></table><p>In this vLLM source tree, <code>_rocm_C</code> consists of three files:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">csrc/rocm/torch_bindings.cpp</span><br><span class="line">csrc/rocm/skinny_gemms.cu</span><br><span class="line">csrc/rocm/attention.cu</span><br></pre></td></tr></table></figure><p><code>skinny_gemms.cu</code> and <code>attention.cu</code> first go through hipify, then DTK’s compiler generates target code for <code>gfx936</code>, and finally they are linked together with the operator registration code into <code>_rocm_C.abi3.so</code>.</p><h2 id="A-kernel-has-at-least-six-“identities”-before-it-runs"><a href="#A-kernel-has-at-least-six-“identities”-before-it-runs" class="headerlink" title="A kernel has at least six “identities” before it runs"></a>A kernel has at least six “identities” before it runs</h2><p>Looking only at the source directory, it is easy to mistakenly think that a modification has already taken effect. The actual chain is much longer:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">skinny_gemms.cu</span><br><span class="line">        |</span><br><span class="line">        | hipify</span><br><span class="line">        v</span><br><span class="line">skinny_gemms.hip</span><br><span class="line">        |</span><br><span class="line">        | clang++ --offload-arch=gfx936</span><br><span class="line">        v</span><br><span class="line">cmake-build-rocm/_rocm_C.abi3.so</span><br><span class="line">        |</span><br><span class="line">        | cmake --install / manual install</span><br><span class="line">        v</span><br><span class="line">vllm/_rocm_C.abi3.so in the Python environment</span><br><span class="line">        |</span><br><span class="line">        | loaded by the service process</span><br><span class="line">        v</span><br><span class="line">loaded shared library in /proc/&lt;pid&gt;/maps</span><br><span class="line">        |</span><br><span class="line">        | runtime conditions such as shape, dtype, layout</span><br><span class="line">        v</span><br><span class="line">kernel branch actually selected for this request</span><br></pre></td></tr></table></figure><p><img src="/img/dcu/dcu-kernel-identity-chain.png" alt="The six-layer identity chain from source code to real dispatch for a vLLM kernel on Hygon DCU"></p><p>If any layer here is not aligned, a very dangerous state appears: the program runs, but it is not running the implementation you think it is.</p><p>Therefore, “compilation succeeded” can at most prove that the third layer generated a file. It cannot prove that the file was installed into the current Python environment, still less that the already-started vLLM process reloaded it, and finally it cannot prove that the target request actually hit the new dispatch.</p><h2 id="The-incremental-compilation-I-actually-got-working"><a href="#The-incremental-compilation-I-actually-got-working" class="headerlink" title="The incremental compilation I actually got working"></a>The incremental compilation I actually got working</h2><p>Given an existing build directory, after modifying <code>csrc/rocm/skinny_gemms.cu</code>, I use the following two commands:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cmake --build --preset rocm-release --target _rocm_C</span><br><span class="line">cmake --install cmake-build-rocm --component _rocm_C</span><br></pre></td></tr></table></figure><p>In this environment there is a not-so-obvious difference: when only <code>CMakeUserPresets.json</code> was placed, the current image’s build process did not recognize it correctly; it worked stably only after switching to the actually used <code>CMakePresets.json</code>. The preset must at least specify the following information:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">VLLM_TARGET_DEVICE=rocm</span><br><span class="line">PYTORCH_ROCM_ARCH=gfx936</span><br><span class="line">ROCM_PATH=/opt/dtk</span><br><span class="line">CMAKE_HIP_COMPILER_LAUNCHER=ccache</span><br></pre></td></tr></table></figure><p>A real incremental build log can be compressed into the following lines:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">[1/4] ... hipify.py ... skinny_gemms.cu attention.cu</span><br><span class="line">skinny_gemms.cu -&gt; skinny_gemms.hip [ok]</span><br><span class="line">attention.cu -&gt; attention.hip [skipped, already hipified]</span><br><span class="line">Successfully preprocessed all matching files.</span><br><span class="line">Total number of unsupported CUDA function calls: 0</span><br><span class="line">Total number of replaced kernel launches: 19</span><br><span class="line"></span><br><span class="line">[2/3] clang++ ... --offload-arch=gfx936 ... skinny_gemms.hip</span><br><span class="line">[3/3] ... -o _rocm_C.abi3.so</span><br></pre></td></tr></table></figure><p>These lines are more useful than the final <code>build success</code>. They simultaneously prove four things:</p><ol><li>The modified <code>.cu</code> was re-hipified;</li><li>The unmodified <code>attention.cu</code> was not pointlessly re-converted;</li><li>The compilation command really targeted <code>gfx936</code>;</li><li>A shared library relink actually occurred.</li></ol><p>The same local experiment also recorded the state of the generated extension:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">size: 21018624 bytes</span><br><span class="line">sha256: 0e3280734c80bf7d33e101c04eecd9cdee0844feb3447b2141a3326ad6ef7549</span><br><span class="line">torch: 2.10.0</span><br><span class="line">LLMM1_registered: True</span><br></pre></td></tr></table></figure><p>This hash is only used to identify that particular binary at that time and does not mean that other correct builds should produce the same hash.</p><h3 id="A-shared-directory-can-slow-down-the-configuration-phase"><a href="#A-shared-directory-can-slow-down-the-configuration-phase" class="headerlink" title="A shared directory can slow down the configuration phase"></a>A shared directory can slow down the configuration phase</h3><p>I also compared the CMake configure time for the same source tree in two locations:</p><table><thead><tr><th>Build location</th><th align="right">Measured configure time</th></tr></thead><tbody><tr><td>Shared directory</td><td align="right">about 111 seconds</td></tr><tr><td>Container-local <code>/root</code></td><td align="right">13.5 seconds</td></tr></tbody></table><p><img src="/img/dcu/dcu-cmake-configure-time.png" alt="CMake configure time comparison between a shared directory and a container-local directory"></p><p>The difference is about 8.2x. This result does not mean that <code>/root</code> will be 8.2x faster on any machine; it means that when the source tree, CMake probes, and a large number of small-file operations all land on high-latency shared storage, the configuration phase itself can become an iteration bottleneck.</p><p>My approach is to put the temporary source overlay and <code>cmake-build-rocm</code> on the container-local disk, and after compilation only copy the logs, source patches, and final <code>.so</code> that need to be preserved back to the persistent directory. This keeps the evidence while avoiding repeated metadata access on the shared directory for every configure.</p><h2 id="Four-“looks-successful”-failure-states"><a href="#Four-“looks-successful”-failure-states" class="headerlink" title="Four “looks successful” failure states"></a>Four “looks successful” failure states</h2><p>I have encountered all four of the following situations in actual debugging. They are troublesome because the service does not necessarily report an error.</p><h3 id="1-import-vllm-succeeds-but-the-ROCm-extension-does-not-exist"><a href="#1-import-vllm-succeeds-but-the-ROCm-extension-does-not-exist" class="headerlink" title="1. import vllm succeeds, but the ROCm extension does not exist"></a>1. <code>import vllm</code> succeeds, but the ROCm extension does not exist</h3><p>After reinstalling a wheel once, the Python package was restored and <code>import vllm</code> worked normally, but the default installation did not contain <code>_rocm_C.abi3.so</code>. If you verify the environment only with <code>import vllm</code>, you will mistake “the Python package exists” for “the custom ROCm extension is installed”.</p><p>A more reliable check is to import the extension directly and confirm that the target operator has been registered:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">python3 - &lt;&lt;<span class="string">&#x27;PY&#x27;</span></span><br><span class="line">import importlib.util</span><br><span class="line">import os</span><br><span class="line">import torch</span><br><span class="line"></span><br><span class="line">spec = importlib.util.find_spec(<span class="string">&quot;vllm._rocm_C&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;extension:&quot;</span>, None <span class="keyword">if</span> spec is None <span class="keyword">else</span> spec.origin)</span><br><span class="line"><span class="keyword">if</span> spec is None:</span><br><span class="line">    raise SystemExit(<span class="string">&quot;vllm._rocm_C is missing&quot;</span>)</span><br><span class="line"></span><br><span class="line">import vllm._rocm_C  <span class="comment"># noqa: F401</span></span><br><span class="line">op_name = os.environ.get(<span class="string">&quot;DCU_VERIFY_OP&quot;</span>, <span class="string">&quot;LLMM1&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;registered:&quot;</span>, op_name, hasattr(torch.ops._rocm_C, op_name))</span><br><span class="line">PY</span><br></pre></td></tr></table></figure><p>Here <code>LLMM1</code> is the operator name I was verifying at the time. When switching to another project, set <code>DCU_VERIFY_OP</code> to the registration name you actually need to test.</p><h3 id="2-After-reinstalling-the-wheel-only-one-currently-modified-file-was-copied"><a href="#2-After-reinstalling-the-wheel-only-one-currently-modified-file-was-copied" class="headerlink" title="2. After reinstalling the wheel, only one currently modified file was copied"></a>2. After reinstalling the wheel, only one currently modified file was copied</h3><p>In another round of experiments, reinstalling the wheel restored <code>site-packages</code> to the wheel’s default state. At that time I copied only the Python file I was currently modifying, but missed other modified files inherited by the candidate implementation. The service could still start and the benchmark could complete, but that was not the complete candidate, and the results had no comparative value.</p><p>Since then, I no longer decide the synchronization scope by “how many files I manually changed this time”; instead, I decide it by the complete source diff:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git diff --name-only &lt;baseline&gt;...HEAD</span><br></pre></td></tr></table></figure><p>All differing files that can affect runtime must be installed, hashed, and included in the backup and restore list. For Python files, <code>python -m py_compile</code> should also be run before starting the service, to avoid leaving transport corruption, encoding anomalies, or incomplete files to be discovered only at the model-loading stage tens of minutes later.</p><h3 id="3-The-so-on-disk-is-updated-but-the-old-process-is-still-using-the-original-mapping"><a href="#3-The-so-on-disk-is-updated-but-the-old-process-is-still-using-the-original-mapping" class="headerlink" title="3. The .so on disk is updated, but the old process is still using the original mapping"></a>3. The <code>.so</code> on disk is updated, but the old process is still using the original mapping</h3><p>Overwriting <code>_rocm_C.abi3.so</code> will not cause an already-running Python process to automatically unload the old shared library. After replacing the binary, the service must be fully restarted; if CUDAGraph is used, graph capture should also be redone, and the graph state from the old process must not be reused.</p><p>I once encountered a more insidious situation: after the previous service exited, a <code>VLLM::EngineCore</code> process was still alive and occupying GPU memory. Although the next service started, the environment was no longer clean.</p><p>Before starting a new service, I now perform at least two read-only checks:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ps -eo pid,ppid,cmd | grep -E <span class="string">&#x27;vllm|VLLM::EngineCore&#x27;</span> | grep -v grep</span><br><span class="line">ss -ltnp | grep <span class="string">&#x27;:8000 &#x27;</span></span><br></pre></td></tr></table></figure><p>Confirm that the old service, EngineCore, benchmark driver, and listening port have all been cleaned up, then restart through the normal service management method. After startup, you can also inspect the mappings of the target process:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">PID=$(pgrep -n -f <span class="string">&#x27;VLLM::EngineCore&#x27;</span>)</span><br><span class="line">grep <span class="string">&#x27;_rocm_C.abi3.so&#x27;</span> <span class="string">&quot;/proc/<span class="variable">$&#123;PID&#125;</span>/maps&quot;</span></span><br></pre></td></tr></table></figure><p>It can at least answer “from which path did the current process load the extension”. If the path is not as expected, there is no need to continue analyzing the subsequent performance data.</p><h3 id="4-Same-filename-but-different-binary-origin"><a href="#4-Same-filename-but-different-binary-origin" class="headerlink" title="4. Same filename, but different binary origin"></a>4. Same filename, but different binary origin</h3><p>Once, while cleaning up the environment, I recorded that the hash prefix of <code>_rocm_C</code> in the runtime environment was:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">7aee47576aa8...</span><br></pre></td></tr></table></figure><p>while the hash prefix of the known extension prepared as a control was:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">f2c5393af460...</span><br></pre></td></tr></table></figure><p>Both files are named <code>_rocm_C.abi3.so</code>, and the target operators can run in both, but they are not the same binary. Without a hash record, this difference is easily masked by the filename.</p><p>I now record the hashes of both the build directory and the actual installation location:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">BUILD_SO=cmake-build-rocm/_rocm_C.abi3.so</span><br><span class="line">INSTALLED_SO=$(python3 - &lt;&lt;<span class="string">&#x27;PY&#x27;</span></span><br><span class="line">import importlib.util</span><br><span class="line">spec = importlib.util.find_spec(<span class="string">&quot;vllm._rocm_C&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> spec is None:</span><br><span class="line">    raise SystemExit(1)</span><br><span class="line"><span class="built_in">print</span>(spec.origin)</span><br><span class="line">PY</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="built_in">sha256sum</span> <span class="string">&quot;<span class="variable">$BUILD_SO</span>&quot;</span> <span class="string">&quot;<span class="variable">$INSTALLED_SO</span>&quot;</span></span><br><span class="line">cmp -s <span class="string">&quot;<span class="variable">$BUILD_SO</span>&quot;</span> <span class="string">&quot;<span class="variable">$INSTALLED_SO</span>&quot;</span> \</span><br><span class="line">  &amp;&amp; <span class="built_in">echo</span> <span class="string">&quot;binary match&quot;</span> \</span><br><span class="line">  || <span class="built_in">echo</span> <span class="string">&quot;binary mismatch&quot;</span></span><br></pre></td></tr></table></figure><h2 id="Hash-alone-is-not-enough-you-must-prove-that-the-request-hit-the-new-dispatch"><a href="#Hash-alone-is-not-enough-you-must-prove-that-the-request-hit-the-new-dispatch" class="headerlink" title="Hash alone is not enough: you must prove that the request hit the new dispatch"></a>Hash alone is not enough: you must prove that the request hit the new dispatch</h2><p>Consistent hashes only prove that the service loaded the target extension; they do not prove that a request selected the new kernel. Dispatch in vLLM is often affected simultaneously by shape, dtype, layout, sequence phase, and cache configuration. If even one condition is missed, the program may silently fall back to the original path.</p><p>My approach is to keep a diagnostic marker controlled by an environment variable in the candidate branch, printing only on the line where the candidate kernel is actually selected:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> os.getenv(<span class="string">&quot;VLLM_DCU_KERNEL_DIAG&quot;</span>) == <span class="string">&quot;1&quot;</span>:</span><br><span class="line">    logger.warning(</span><br><span class="line">        <span class="string">&quot;DCU_KERNEL_DIAG selected=%s shape=%s dtype=%s&quot;</span>,</span><br><span class="line">        selected,</span><br><span class="line">        <span class="built_in">tuple</span>(x.shape),</span><br><span class="line">        x.dtype,</span><br><span class="line">    )</span><br></pre></td></tr></table></figure><p>The key to the marker is not “the feature is enabled”; rather, <code>selected=True</code> must come from the actual dispatch result. My automated runner extracts the marker from the service log and treats “expected shape appears, key parameters are correct, selected&#x3D;True” as a precondition for a valid benchmark.</p><p>This is especially important for ROCm kernels on Hygon DCU. The fallback in many experiments is still a usable and reasonably fast vendor or Triton path, so “the service did not report an error” absolutely cannot prove that the new kernel was executed.</p><h2 id="The-seven-verification-gates-I-now-use"><a href="#The-seven-verification-gates-I-now-use" class="headerlink" title="The seven verification gates I now use"></a>The seven verification gates I now use</h2><p>The following table is the check order I finally settled on. If the previous item does not pass, do not proceed to the next.</p><table><thead><tr><th align="right">Order</th><th>What to prove</th><th>Minimum evidence</th></tr></thead><tbody><tr><td align="right">1</td><td>Source state is complete</td><td>Complete diff list, not just currently dirty files</td></tr><tr><td align="right">2</td><td>Compilation target is correct</td><td>Log shows <code>--offload-arch=gfx936</code> and recompilation of the target source file</td></tr><tr><td align="right">3</td><td>The installed binary is new</td><td>SHA256 of build <code>.so</code> matches the actual import path</td></tr><tr><td align="right">4</td><td>Operator registration succeeded</td><td>Directly import <code>vllm._rocm_C</code>, target op is <code>True</code></td></tr><tr><td align="right">5</td><td>Process state is clean</td><td>No leftover EngineCore, old service, old driver, or occupied port</td></tr><tr><td align="right">6</td><td>Real request hits</td><td>Service log shows <code>selected=True</code> marker with real shape</td></tr><tr><td align="right">7</td><td>Results are usable for comparison</td><td>Output, request count, and failure count meet the pre-agreed conditions before discussing performance</td></tr></tbody></table><p>This process looks like a few more steps than “compile and then run the benchmark directly”, but most checks take only seconds. What really takes time is skipping them and then spending hours analyzing a set of data that actually came from an old binary, half the source tree, or a fallback path.</p><h2 id="A-read-only-verification-script"><a href="#A-read-only-verification-script" class="headerlink" title="A read-only verification script"></a>A read-only verification script</h2><p>The following is a compressed version of the common checks I use. It does not modify files and does not stop processes; it is suitable for running once before and once after service startup. The operator name can be replaced through an environment variable.</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/usr/bin/env bash</span></span><br><span class="line"><span class="built_in">set</span> -euo pipefail</span><br><span class="line"></span><br><span class="line"><span class="built_in">export</span> DCU_VERIFY_OP=<span class="string">&quot;<span class="variable">$&#123;DCU_VERIFY_OP:-LLMM1&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line">python3 - &lt;&lt;<span class="string">&#x27;PY&#x27;</span></span><br><span class="line">import importlib.util</span><br><span class="line">import os</span><br><span class="line">import torch</span><br><span class="line">import vllm</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;vllm:&quot;</span>, vllm.__file__)</span><br><span class="line">spec = importlib.util.find_spec(<span class="string">&quot;vllm._rocm_C&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;_rocm_C:&quot;</span>, None <span class="keyword">if</span> spec is None <span class="keyword">else</span> spec.origin)</span><br><span class="line"><span class="keyword">if</span> spec is None:</span><br><span class="line">    raise SystemExit(<span class="string">&quot;missing vllm._rocm_C&quot;</span>)</span><br><span class="line"></span><br><span class="line">import vllm._rocm_C  <span class="comment"># noqa: F401</span></span><br><span class="line">name = os.environ[<span class="string">&quot;DCU_VERIFY_OP&quot;</span>]</span><br><span class="line">ok = hasattr(torch.ops._rocm_C, name)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;op:&quot;</span>, name, <span class="string">&quot;registered=&quot;</span>, ok)</span><br><span class="line"><span class="keyword">if</span> not ok:</span><br><span class="line">    raise SystemExit(<span class="string">&quot;target op is not registered&quot;</span>)</span><br><span class="line">PY</span><br><span class="line"></span><br><span class="line">SO=$(python3 - &lt;&lt;<span class="string">&#x27;PY&#x27;</span></span><br><span class="line">import importlib.util</span><br><span class="line"><span class="built_in">print</span>(importlib.util.find_spec(<span class="string">&quot;vllm._rocm_C&quot;</span>).origin)</span><br><span class="line">PY</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="built_in">ls</span> -l <span class="string">&quot;<span class="variable">$SO</span>&quot;</span></span><br><span class="line"><span class="built_in">sha256sum</span> <span class="string">&quot;<span class="variable">$SO</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;--- active vLLM processes ---&quot;</span></span><br><span class="line">ps -eo pid,ppid,cmd | grep -E <span class="string">&#x27;vllm|VLLM::EngineCore&#x27;</span> | grep -v grep || <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>If the <code>.so</code> in the build directory, the <code>.so</code> actually imported by Python, the <code>.so</code> in the process mapping, and the dispatch marker can form a closed chain of evidence, only then will I write the subsequent results into the experimental conclusion.</p><h2 id="The-boundaries-of-this-method"><a href="#The-boundaries-of-this-method" class="headerlink" title="The boundaries of this method"></a>The boundaries of this method</h2><p>There are three points that need to be made clear in this article:</p><ol><li><code>111 seconds -&gt; 13.5 seconds</code> is an observation under specific container and shared-storage conditions, not a fixed performance metric for Hygon DCU;</li><li><code>21018624</code> bytes and the SHA256 in this article belong to one specific build and are only used to illustrate how to record binary identity;</li><li>This process proves “the test object is correct”; it does not automatically prove that the kernel is faster or numerically correct. Performance and correctness still need separate verification.</li></ol><p>However, for me, this step has already become the engineering habit most worth keeping in Hygon DCU kernel development. HIPify, the DTK compiler, the CMake build tree, the Python installation directory, the long-running vLLM process, and runtime dispatch—each layer may retain stale state. Verifying them layer by layer is far more reliable than staring at the final line <code>build success</code>.</p><p>Now after I finish modifying a kernel, my first reaction is no longer to see how much the number has increased, but to first confirm: is the source correct, is the binary correct, is the process correct, and did this request actually reach it? The extra cost is only a few minutes, and what is often saved is an entire round of erroneous experiments.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/07/17/dcu-vllm-incremental-build-verification/</id>
    <link href="https://luanjiahao.me/2026/07/17/dcu-vllm-incremental-build-verification/"/>
    <published>2026-07-17T01:00:00.000Z</published>
    <summary>
      <![CDATA[<p>The biggest waste of time I encountered while modifying vLLM kernels on Hygon DCU was not a compilation error.</p>
<p>That time, compilation ended normally, the service could start, the endpoint could return results, and the performance test even produced a change that looked explainable. Only af]]>
    </summary>
    <title>Build Success Does Not Mean It Takes Effect — Verifying Incremental vLLM Compilation on Hygon DCU</title>
    <updated>2026-10-28T04:22:26.317Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目日志" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE%E6%97%A5%E5%BF%97/"/>
    <category term="THU-BDC2026" scheme="https://luanjiahao.me/tags/THU-BDC2026/"/>
    <category term="机器学习" scheme="https://luanjiahao.me/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <category term="回测" scheme="https://luanjiahao.me/tags/%E5%9B%9E%E6%B5%8B/"/>
    <category term="实验记录" scheme="https://luanjiahao.me/tags/%E5%AE%9E%E9%AA%8C%E8%AE%B0%E5%BD%95/"/>
    <category term="复盘" scheme="https://luanjiahao.me/tags/%E5%A4%8D%E7%9B%98/"/>
    <content>
      <![CDATA[<h2 id="Conclusion-First"><a href="#Conclusion-First" class="headerlink" title="Conclusion First"></a>Conclusion First</h2><p>What is most worth recording about THU-BDC2026 is not some return that looks beautiful once, but those experiments that pulled the model back from “looks effective” to “actually verifiable”.</p><p>This is my experiment log. It does not seek to package the project as smooth sailing; instead, it specifically writes down several optimizations that made the results worse and worse, because the experience that can truly remain in a competition is often hidden in these failures.</p><table><thead><tr><th>Time</th><th>Experiment</th><th>Result</th><th>My Judgment</th></tr></thead><tbody><tr><td>2026-05-31</td><td>Single-period backtest</td><td>Latest dataset model, single week +5.9533%</td><td>Very bright, but the period is too short; can only be a clue</td></tr><tr><td>2026-06-04</td><td>Fixing data leakage</td><td>Sharpe dropped from 2.30 to -0.49</td><td>Ugly, but finally honest</td></tr><tr><td>2026-06-05</td><td>Removing volatility features</td><td>Features 289 -&gt; 227, Sharpe back to 0.40</td><td>Subtraction works</td></tr><tr><td>2026-06-06</td><td>Adding money flow factors</td><td>Sharpe 0.70, cumulative return 12.33%</td><td>The most decent version so far</td></tr><tr><td>2026-06-06</td><td>Top-80 feature selection</td><td>Training 57% faster, return dropped to -7.47%</td><td>Fast does not equal good</td></tr><tr><td>2026-06-07</td><td>Industry coverage fix</td><td>Coverage 4.7% -&gt; 100%, Sharpe fell to 0.18</td><td>Data completeness does not mean the model can use it</td></tr></tbody></table><h2 id="Why-Write-It-as-a-Log"><a href="#Why-Write-It-as-a-Log" class="headerlink" title="Why Write It as a Log"></a>Why Write It as a Log</h2><p>At the beginning, this project could easily get people carried away: complex models, rich metrics, and some backtest results that looked very strong.</p><p>But what quantitative projects fear most is “abnormally pretty”. As long as there is a little future information in the training set split, label construction, or feature window, the model will become stronger as if it suddenly got it. The problem is that this kind of strength is not real strength; it just saw the answer in advance.</p><p>So the focus of this article is not “how powerful a model I built”, but recording how I squeezed out the fluff step by step:</p><ul><li>First, acknowledge that single-period results cannot be conclusions;</li><li>Then fix the time isolation between training and prediction;</li><li>Next, delete features that may depend on leakage;</li><li>Finally, see which factors can still retain real gains.</li></ul><h2 id="Early-Single-Period-Backtest-Very-Bright-but-Not-to-Be-Trusted-Too-Much"><a href="#Early-Single-Period-Backtest-Very-Bright-but-Not-to-Be-Trusted-Too-Much" class="headerlink" title="Early Single-Period Backtest: Very Bright, but Not to Be Trusted Too Much"></a>Early Single-Period Backtest: Very Bright, but Not to Be Trusted Too Much</h2><p>There is an early backtest report in the archive; it only tested the 5 trading days from 2026-03-09 to 2026-03-13.</p><p>At the time, the results looked very good:</p><table><thead><tr><th>Model</th><th align="right">Single-period return</th></tr></thead><tbody><tr><td>Original dataset model</td><td align="right">-0.7367%</td></tr><tr><td>Latest dataset model</td><td align="right">5.9533%</td></tr><tr><td>Equal-weight benchmark</td><td align="right">1.3114%</td></tr></tbody></table><p>If you only look at this table, it is easy to feel that the direction is already right. But 5 trading days is too short, and luck plays a big role. It is more like a hint: “There may be something here, worth investigating further,” but it cannot prove that the model is stably effective.</p><p>Looking back later, the greatest value of this stage was not the return, but the reminder that I had to lengthen the validation period and make the backtesting process stricter.</p><h2 id="Fixing-Data-Leakage-The-Most-Painful-but-the-Most-Worthwhile"><a href="#Fixing-Data-Leakage-The-Most-Painful-but-the-Most-Worthwhile" class="headerlink" title="Fixing Data Leakage: The Most Painful, but the Most Worthwhile"></a>Fixing Data Leakage: The Most Painful, but the Most Worthwhile</h2><p>What truly changed the direction of the project was fixing data leakage.</p><p>The original backtest only ensured that training sample dates were earlier than the backtest date, but the labels used the opening prices of the next 5 days. That is, for training samples close to the backtest date, although the dates were in the past, the labels might already extend into the future window.</p><p>This type of leakage is very hidden. The code looks fine, and the results look very good, but in essence the model has already peeked at the answer.</p><p>After the fix, the results immediately became ugly:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Sharpe:        -0.49</span><br><span class="line">Cumulative portfolio:     -17.11%</span><br><span class="line">Win rate vs. equal-weight: 44.8%</span><br><span class="line">Max drawdown:     -31.17%</span><br></pre></td></tr></table></figure><p>This step was very discouraging, but it was the most important step in the entire experiment line. Because from here on, the later results are worth discussing.</p><h2 id="Subtraction-Cutting-Volatility-Features"><a href="#Subtraction-Cutting-Volatility-Features" class="headerlink" title="Subtraction: Cutting Volatility Features"></a>Subtraction: Cutting Volatility Features</h2><p>After fixing the leakage, I began to suspect a batch of volatility-related features.</p><p>For example:</p><ul><li><code>boll_width</code></li><li><code>atr_pct</code></li><li><code>vol_20d</code></li><li>Aggregate features and cross-sectional rank features derived from these indicators</li></ul><p>They had very high importance in the old version, but the old version itself had leakage. So I was not sure whether they were truly effective or just helped the model memorize shortcuts in the wrong validation.</p><p>So I ran a deletion experiment:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Feature dimension: 289 -&gt; 227</span><br><span class="line">Sharpe:     -0.49 -&gt; 0.40</span><br><span class="line">Cumulative return: -17.11% -&gt; 4.39%</span><br><span class="line">Win rate:     44.8% -&gt; 51.7%</span><br></pre></td></tr></table></figure><p>This time the results actually got better. The reminder it gave me was very direct: more features are not safer. Especially in financial data, some features seem to have a lot of information, but in fact they just amplify noise and wrong validation together.</p><h2 id="Money-Flow-One-of-the-Few-Things-That-Truly-Brought-Gains"><a href="#Money-Flow-One-of-the-Few-Things-That-Truly-Brought-Gains" class="headerlink" title="Money Flow: One of the Few Things That Truly Brought Gains"></a>Money Flow: One of the Few Things That Truly Brought Gains</h2><p>Later I added Tushare money-flow-related factors, mainly including three categories:</p><ul><li>Main-force net inflow rate;</li><li>Small-order net inflow rate;</li><li>Super-large-order share.</li></ul><p>This version is a relatively decent one in the current experiment log:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Sharpe:         0.70</span><br><span class="line">Cumulative portfolio:     12.33%</span><br><span class="line">Annualized return:     29.90%</span><br><span class="line">Win rate vs. equal-weight: 51.7%</span><br><span class="line">Max drawdown:     -25.13%</span><br></pre></td></tr></table></figure><p>More importantly, <code>主力净流入率_ma20_cs_rank</code> entered the top 20 feature importances.</p><p>This made me feel that capital behavior signals may be more valuable than simply stacking price patterns. Price patterns easily overfit; money flow at least provided another type of information in this window.</p><h2 id="Three-Failed-Optimizations"><a href="#Three-Failed-Optimizations" class="headerlink" title="Three Failed Optimizations"></a>Three Failed Optimizations</h2><h3 id="Top-80-Feature-Selection"><a href="#Top-80-Feature-Selection" class="headerlink" title="Top-80 Feature Selection"></a>Top-80 Feature Selection</h3><p>I used data before 2023 to select the 80 most important features, wanting to reduce noise and speed up training.</p><p>Training speed did indeed become faster:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Training time: 116 minutes -&gt; 50 minutes</span><br><span class="line">Speed improvement: about 57%</span><br></pre></td></tr></table></figure><p>But the results got worse:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Cumulative return: +12.33% -&gt; -7.47%</span><br><span class="line">Sharpe:     0.70 -&gt; -0.07</span><br></pre></td></tr></table></figure><p>This shows that importance in a historical window does not necessarily transfer to the future. Financial data is not a static dataset; once the market regime changes, the features that were most important in the past may instead hold it back.</p><h3 id="Three-Class-Labels"><a href="#Three-Class-Labels" class="headerlink" title="Three-Class Labels"></a>Three-Class Labels</h3><p>I also tried roughly dividing stocks into three tiers:</p><ul><li>Top 30%;</li><li>Middle 40%;</li><li>Bottom 30%.</li></ul><p>The intention was to make the model focus more on the “buy zone” and the “risk-avoidance zone”, but the actual effect was very poor, and the Sharpe fell to -1.08.</p><p>The reason is probably that a ranking task cannot be cut too coarsely. Although 10-bin labels are a bit more complex, they retain more relative order information and are more suitable for selecting the Top 5 from 300 stocks every day.</p><h3 id="Industry-Coverage-Fix"><a href="#Industry-Coverage-Fix" class="headerlink" title="Industry Coverage Fix"></a>Industry Coverage Fix</h3><p>There was also a very counterintuitive failure: the industry coverage fix.</p><p>Originally industry coverage was only 4.7%, which was certainly unreasonable, so I filled the industry information to 100%. From the perspective of data completeness, this step was absolutely correct.</p><p>But model performance dropped from a Sharpe of 0.70 to 0.18, and <code>sector_id</code> became the most important feature.</p><p>This shows that the model may not have “learned the industry” but rather “trusted the industry too much”. Industry information needs finer constraints, such as downweighting, regularization, and grouped validation; it cannot just be stuffed in all at once.</p><h2 id="Rules-Left-in-the-End"><a href="#Rules-Left-in-the-End" class="headerlink" title="Rules Left in the End"></a>Rules Left in the End</h2><p>After this round of experiments, I left myself a few rules:</p><ol><li>Single-period results can only be clues, not conclusions.</li><li>The first step of backtesting is always to check for data leakage.</li><li>Feature importance should be viewed by time period; one historical window cannot be treated as truth.</li><li>Subtraction is not shameful; being able to remove noise is also a skill.</li><li>More complete data also needs to be used correctly by the model.</li><li>All changes that “look reasonable” must ultimately let the backtest speak.</li></ol><h2 id="What-to-Do-Next"><a href="#What-to-Do-Next" class="headerlink" title="What to Do Next"></a>What to Do Next</h2><p>If I continue advancing this project, I will prioritize three things:</p><ul><li>Lengthen the rolling backtest period and stop being led by single-week returns;</li><li>Perform grouped validation on strongly structural features such as industry, market cap, and liquidity;</li><li>Keep LightGBM LambdaRank as the main line, and treat deep models as auxiliary experiments rather than the main battlefield.</li></ul><p>This record does not constitute any investment advice. It is more like a competition experiment memo: break open the good-looking results, keep the ugly-looking results, and only what remains in the end may be real experience.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/06/13/thu-bdc2026-experiment-log/</id>
    <link href="https://luanjiahao.me/2026/06/13/thu-bdc2026-experiment-log/"/>
    <published>2026-06-13T03:20:00.000Z</published>
    <summary>A public version experiment record covering single-period high return, data leakage fix, feature pruning, money flow factors, and several failed optimizations.</summary>
    <title>THU-BDC2026 Experiment Log - Those Optimizations That Made the Results Worse and Worse</title>
    <updated>2026-06-13T03:18:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE/"/>
    <category term="THU-BDC2026" scheme="https://luanjiahao.me/tags/THU-BDC2026/"/>
    <category term="LightGBM" scheme="https://luanjiahao.me/tags/LightGBM/"/>
    <category term="Transformer" scheme="https://luanjiahao.me/tags/Transformer/"/>
    <category term="LambdaRank" scheme="https://luanjiahao.me/tags/LambdaRank/"/>
    <category term="特征工程" scheme="https://luanjiahao.me/tags/%E7%89%B9%E5%BE%81%E5%B7%A5%E7%A8%8B/"/>
    <content>
      <![CDATA[<h2 id="Why-Write-a-Separate-Technical-Route-Post"><a href="#Why-Write-a-Separate-Technical-Route-Post" class="headerlink" title="Why Write a Separate Technical Route Post"></a>Why Write a Separate Technical Route Post</h2><p>The previous post was more like a competition retrospective; this one is specifically to sort out the technical route for THU-BDC2026.</p><p>For this project, from the earliest deep model to the later LightGBM LambdaRank, the transition was not simply “switching models”; rather, my understanding of the problem changed: the stock ranking task is not just about predicting returns, but more like making cross-sectional comparisons among 300 stocks on the same day.</p><p>So afterward I focused more and more on three things:</p><ul><li>the daily cross-sectional ranking;</li><li>the temporal separation between training and prediction;</li><li>whether the features can truly carry across different years.</li></ul><h2 id="Phase-One-Transformer-Cross-Sectional-Modeling"><a href="#Phase-One-Transformer-Cross-Sectional-Modeling" class="headerlink" title="Phase One: Transformer + Cross-Sectional Modeling"></a>Phase One: Transformer + Cross-Sectional Modeling</h2><p>The earliest design was StockTransformer.</p><p>The input used data from the past 30 trading days; each stock had 72 technical indicators, organized overall as:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[batch, 300只股票, 30天, 72特征]</span><br></pre></td></tr></table></figure><p>The model had several key modules:</p><ul><li>TransformerEncoder: processes the time series of a single stock;</li><li>FeatureAttention: aggregates time-series information at different scales;</li><li>CrossStockAttention: lets stocks on the same day “see” each other;</li><li>MoE Ranking: uses multiple expert heads for ranking scores;</li><li>Score Head: outputs the final score for each stock.</li></ul><p>The advantage of this design was strong expressiveness, and it also aligned with the intuition that stocks have relative relationships. The drawbacks were also obvious: slow training, high tuning cost, inconvenient backtest iteration, and the validation method was slightly problematic; complex models amplify the problem.</p><h2 id="Phase-Two-Turning-to-LambdaRank"><a href="#Phase-Two-Turning-to-LambdaRank" class="headerlink" title="Phase Two: Turning to LambdaRank"></a>Phase Two: Turning to LambdaRank</h2><p>Later I switched the main iteration to LightGBM’s <code>lambdarank</code>.</p><p>The reasons were practical:</p><p>First, the competition objective is essentially Top 5 ranking, and it is not necessarily about predicting an absolute return. Treating the 300 stocks each day as a group and directly optimizing NDCG@3 &#x2F; NDCG@5 with a ranking objective is closer to the task.</p><p>Second, LightGBM trains much faster. A rolling backtest still takes a long time, but it is more suitable for repeated trial and error than a deep model.</p><p>Third, the feature importance of tree models is easier to inspect. It does not necessarily represent causality, but at least it lets me discover whether the model is being skewed by some weird type of feature.</p><h2 id="Labels-and-Grouping"><a href="#Labels-and-Grouping" class="headerlink" title="Labels and Grouping"></a>Labels and Grouping</h2><p>The label definition is:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(T+5 开盘价 - T+1 开盘价) / T+1 开盘价</span><br></pre></td></tr></table></figure><p>That is, the model predicts the stage return from T+1 to T+5. To make different trading days comparable, I process cross-sectionally by date, then treat the stocks on the same day as one ranking group.</p><p>This is where problems most easily occur.</p><p>If the backtest date is <code>bt_date</code>, the training set cannot just be written as:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">processed[<span class="string">&#x27;日期&#x27;</span>] &lt; bt_date</span><br></pre></td></tr></table></figure><p>Because for samples close to <code>bt_date</code>, their label may already use future opening prices. Later I changed it to leave at least a gap, to avoid the training set peeking at information near the prediction.</p><p>This step is more important than changing any model.</p><h2 id="Feature-Engineering"><a href="#Feature-Engineering" class="headerlink" title="Feature Engineering"></a>Feature Engineering</h2><p>The features used in this project roughly fall into several categories:</p><table><thead><tr><th>Type</th><th>Example</th><th>Purpose</th></tr></thead><tbody><tr><td>Trend&#x2F;position</td><td><code>ma_dev_60d</code>, <code>price_pos_20d</code></td><td>Describe the price position relative to moving averages and the range</td></tr><tr><td>Candlestick pattern</td><td><code>lower_shadow_ratio</code>, <code>upper_shadow_ratio</code></td><td>Describe intraday buying and selling forces</td></tr><tr><td>Volume-price relationship</td><td><code>corr_pv_20</code>, <code>obv_ratio</code></td><td>Describe the relationship between volume and price changes</td></tr><tr><td>Cross-sectional rank</td><td><code>*_cs_rank</code></td><td>Make relative comparisons among 300 stocks on the same day</td></tr><tr><td>Market cap&#x2F;industry</td><td><code>cap_bin</code>, <code>sector_id</code></td><td>Provide structural information</td></tr><tr><td>Capital flow</td><td><code>主力净流入率</code>, <code>超大单占比</code></td><td>Capture capital behavior signals</td></tr></tbody></table><p>Later I became increasingly fond of “cross-sectional ranking”. Because in stock competitions, absolute values are often not that stable, but who is stronger or weaker on the same day is closer to the task itself.</p><h2 id="Several-Key-Versions"><a href="#Several-Key-Versions" class="headerlink" title="Several Key Versions"></a>Several Key Versions</h2><table><thead><tr><th>Version</th><th>Main change</th><th align="right">Sharpe</th><th align="right">Cumulative return</th></tr></thead><tbody><tr><td>Baseline after fixing leakage</td><td>Exclude training samples containing future information</td><td align="right">-0.49</td><td align="right">-17.11%</td></tr><tr><td>CutVol</td><td>Remove volatility-related features</td><td align="right">0.40</td><td align="right">4.39%</td></tr><tr><td>Tushare enhancement</td><td>Add capital flow and industry information</td><td align="right">0.70</td><td align="right">12.33%</td></tr><tr><td>Top-80 feature selection</td><td>Keep only the top 80 by historical importance</td><td align="right">-0.07</td><td align="right">-7.47%</td></tr><tr><td>Three-class label</td><td>Top 30% &#x2F; middle &#x2F; bottom 30%</td><td align="right">-1.08</td><td align="right">-25.77%</td></tr><tr><td>Industry coverage fix</td><td>Industry coverage from 4.7% to 100%</td><td align="right">0.18</td><td align="right">-0.48%</td></tr></tbody></table><p>These values are staged backtest results, not official rankings, and not investment advice.</p><h2 id="The-Most-Counterintuitive-Parts"><a href="#The-Most-Counterintuitive-Parts" class="headerlink" title="The Most Counterintuitive Parts"></a>The Most Counterintuitive Parts</h2><p>Several conclusions in this project are quite counterintuitive.</p><p>First, cutting features improves performance. Volatility features were conspicuous when there was data leakage; after the fix, they instead dragged the model down. It reminded me: feature importance is not an eternal truth; it depends on the validation method.</p><p>Second, feature selection worsens performance. The Top-80 version trained much faster, but its cross-year performance was poor. In financial data, what was important in the past does not necessarily remain important in the future.</p><p>Third, fixing industry coverage also worsens performance. Industry data was fixed from 4.7% to 100%; by rights the metadata was cleaner, but the model weighted <code>sector_id</code> too heavily, and the result declined instead.</p><p>All three results point to one thing: quantitative models cannot be accepted based on intuition; they must be accepted through strict backtesting.</p><h2 id="The-Route-I-Now-Prefer"><a href="#The-Route-I-Now-Prefer" class="headerlink" title="The Route I Now Prefer"></a>The Route I Now Prefer</h2><p>If I were to do it again, I would not start by stacking very complex deep structures.</p><p>I would first use LightGBM LambdaRank to build a clean, fast, interpretable baseline, and make the following solid:</p><ul><li>date grouping and label isolation;</li><li>rolling backtest;</li><li>robustness check after removing the best month;</li><li>feature importance drift;</li><li>ablation of a single change;</li><li>weight constraints on prediction results.</li></ul><p>After these are stable, then consider bringing the deep model back for ensembling.</p><p>This project ultimately made me believe more: the truly advanced part in a competition is not how impressive the model name sounds, but whether you can prove your results are not accidental, not peeking at the future, and not fooled by some weird feature.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/06/13/thu-bdc2026-model-iteration/</id>
    <link href="https://luanjiahao.me/2026/06/13/thu-bdc2026-model-iteration/"/>
    <published>2026-06-13T03:10:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Why-Write-a-Separate-Technical-Route-Post"><a href="#Why-Write-a-Separate-Technical-Route-Post" class="headerlink" title="Why Write a Separate Technical Route Post"></a>Why Write a Separate Technical Route Post</h2><p>The previous post was more like a competition retrospective; this one is s]]>
    </summary>
    <title>THU-BDC2026 Technical Route: From Transformer to LightGBM Ranking</title>
    <updated>2026-10-28T04:22:26.321Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="比赛复盘" scheme="https://luanjiahao.me/categories/%E6%AF%94%E8%B5%9B%E5%A4%8D%E7%9B%98/"/>
    <category term="THU-BDC2026" scheme="https://luanjiahao.me/tags/THU-BDC2026/"/>
    <category term="量化" scheme="https://luanjiahao.me/tags/%E9%87%8F%E5%8C%96/"/>
    <category term="机器学习" scheme="https://luanjiahao.me/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <category term="回测" scheme="https://luanjiahao.me/tags/%E5%9B%9E%E6%B5%8B/"/>
    <category term="数据泄露" scheme="https://luanjiahao.me/tags/%E6%95%B0%E6%8D%AE%E6%B3%84%E9%9C%B2/"/>
    <content>
      <![CDATA[<h2 id="Origin"><a href="#Origin" class="headerlink" title="Origin"></a>Origin</h2><p>For this THU-BDC2026, I actually kept three project directories: the original, archive, and v2. They look like three projects, but after going through them, a more accurate description is: this is the three-stage growth history of the same competition project.</p><p>The problem itself is very direct: based on the historical market data of CSI 300 constituent stocks, rank the stocks, select the Top 5 to form a portfolio, and then assign weights to them. Put plainly, it asks the model to answer a very practical question: over the next period, which stocks deserve to be ranked higher?</p><p>At first I was very excited, because this problem naturally lends itself to writing something very “advanced”: Transformer, CrossStockAttention, MoE, cross-sectional standardization, Top 5 softmax weights. Stacking up the keywords looked beautiful, and the code really did run.</p><p>But later I found that the scariest thing in finance competitions is not that the model is not complex enough, but that the model looks too right.</p><h2 id="First-Version-Looked-Impressive"><a href="#First-Version-Looked-Impressive" class="headerlink" title="First Version: Looked Impressive"></a>First Version: Looked Impressive</h2><p>The earliest approach was the deep model route.</p><p>I used the past 30 trading days as the time window, calculated 72 technical indicators for each stock, and then constructed a four-dimensional tensor:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[batch, 股票数, 30天, 特征数]</span><br></pre></td></tr></table></figure><p>The model structure was roughly:</p><ul><li>StockTransformer to model time series;</li><li>CrossStockAttention to model relationships between different stocks on the same day;</li><li>MoE Ranking to increase model capacity;</li><li>Finally output a ranking score for each stock and take the Top 5.</li></ul><p>From an engineering perspective, this solution was quite complete, and even had a bit of a “competition flavor.” But in quant, you cannot just look at whether the structure is pretty; the core is still whether the backtest can withstand scrutiny.</p><h2 id="The-Bug-That-Calmed-Me-Down"><a href="#The-Bug-That-Calmed-Me-Down" class="headerlink" title="The Bug That Calmed Me Down"></a>The Bug That Calmed Me Down</h2><p>Later I found a very serious problem in the backtest code: training data filtering only used data before the prediction date.</p><p>The original logic was similar to this:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">train_proc = processed[processed[<span class="string">&#x27;日期&#x27;</span>] &lt; bt_date].copy()</span><br></pre></td></tr></table></figure><p>The problem is that the labels were calculated using the opening prices of the next 5 days. That is, although the samples from the few days before the prediction date had dates earlier than the prediction date, their labels might already contain future price information around the prediction date.</p><p>After the fix, I separated the training samples from the prediction date:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">train_proc = processed[</span><br><span class="line">    (processed[<span class="string">&#x27;日期&#x27;</span>] &lt; bt_date) &amp;</span><br><span class="line">    (processed[<span class="string">&#x27;日期&#x27;</span>] + pd.Timedelta(days=<span class="number">10</span>) &lt; bt_date)</span><br><span class="line">].copy()</span><br></pre></td></tr></table></figure><p>After this cut, the results were very ugly.</p><table><thead><tr><th>Metric</th><th align="right">Claimed Before Fix</th><th align="right">Backtest After Fix</th></tr></thead><tbody><tr><td>Sharpe</td><td align="right">2.30</td><td align="right">-0.49</td></tr><tr><td>Cumulative Return</td><td align="right">-</td><td align="right">-17.11%</td></tr><tr><td>Win Rate vs Equal Weight</td><td align="right">-</td><td align="right">44.8%</td></tr><tr><td>Max Drawdown</td><td align="right">-</td><td align="right">-31.17%</td></tr></tbody></table><p>The worst part was not the loss, but that I had to admit: that beautiful result from before was not trustworthy.</p><h2 id="Starting-Over-with-a-More-Honest-Version"><a href="#Starting-Over-with-a-More-Honest-Version" class="headerlink" title="Starting Over with a More Honest Version"></a>Starting Over with a More Honest Version</h2><p>After fixing the data leakage, I did not continue piling things onto the model. Instead, I first did subtraction.</p><p>I found that some volatility-related features were especially “useful” when leakage existed, but after the fix they acted more like noise sources. So I removed <code>vol_5d</code>, <code>vol_10d</code>, <code>vol_20d</code>, <code>boll_pctb</code>, <code>boll_width</code>, and <code>atr_pct</code>.</p><p>The feature dimension dropped from 289 to 227, and the backtest actually got better:</p><table><thead><tr><th>Version</th><th align="right">Sharpe</th><th align="right">Cumulative Return</th><th align="right">Win Rate</th></tr></thead><tbody><tr><td>Baseline after leakage fix</td><td align="right">-0.49</td><td align="right">-17.11%</td><td align="right">44.8%</td></tr><tr><td>Removing volatility features</td><td align="right">0.40</td><td align="right">4.39%</td><td align="right">51.7%</td></tr></tbody></table><p>This step gave me a big reminder: more features is not always better. Some features look especially smart under the wrong validation method, but once validation becomes clean, they expose themselves.</p><h2 id="Tushare-Enhanced-Version-A-More-Decent-One"><a href="#Tushare-Enhanced-Version-A-More-Decent-One" class="headerlink" title="Tushare-Enhanced Version: A More Decent One"></a>Tushare-Enhanced Version: A More Decent One</h2><p>Later I added Tushare’s capital flow factors and Shenwan industry classification, mainly including:</p><ul><li>Main force net inflow rate;</li><li>Small order net inflow rate;</li><li>Extra-large order proportion;</li><li>Industry and market cap related features.</li></ul><p>This version had the best staged backtest performance:</p><table><thead><tr><th>Metric</th><th align="right">Tushare-Enhanced Version</th></tr></thead><tbody><tr><td>Sharpe</td><td align="right">0.70</td></tr><tr><td>Cumulative Return</td><td align="right">12.33%</td></tr><tr><td>Annualized Return</td><td align="right">29.90%</td></tr><tr><td>Win Rate vs Equal Weight</td><td align="right">51.7%</td></tr><tr><td>Max Drawdown</td><td align="right">-25.13%</td></tr></tbody></table><p>Among them, <code>主力净流入率_ma20_cs_rank</code> entered the Top 20 feature importance, which shows that capital flow factors really did provide some extra information.</p><p>But I also do not want to write this as “finally succeeded.” Because the backtest window was only a rolling test from 2024 to 2026, and after removing the best months, return stability clearly weakens. It is a staged better version, not a god model that can be bragged about for a lifetime.</p><h2 id="The-Attempts-That-Didn’t-Improve"><a href="#The-Attempts-That-Didn’t-Improve" class="headerlink" title="The Attempts That Didn’t Improve"></a>The Attempts That Didn’t Improve</h2><p>There were several interesting failures later in this project.</p><p>I tried Top-80 feature selection. Training time did drop from 116 minutes to 50 minutes, but Sharpe fell from 0.70 to -0.07. The reason is probably that important features from before 2023 do not necessarily transfer to 2024-2026.</p><p>I also tried three-class labels, splitting stocks into “top 30% &#x2F; middle 40% &#x2F; bottom 30%.” The result was even worse, with Sharpe falling to -1.08. Later I thought it made sense: a stock ranking task needs to retain as much relative order information as possible, and crude three-class classification erases the middle information instead.</p><p>Another time I fixed industry coverage from 4.7% to 100%. It sounds like it must be right, but the backtest fell from 0.70 to 0.18. This result is very counterintuitive, and also quite real: more complete data does not mean the model is necessarily better. If the model relies too heavily on industry features, fixing metadata may also lead it astray.</p><h2 id="Biggest-Takeaway"><a href="#Biggest-Takeaway" class="headerlink" title="Biggest Takeaway"></a>Biggest Takeaway</h2><p>The biggest lesson this competition gave me was not some model structure, nor some metric, but four words: don’t fool yourself.</p><p>In financial machine learning, the most dangerous thing is “looks reasonable.” Code runs, curves look good, metrics are high—none of these automatically prove that the model really has predictive power.</p><p>What really matters is:</p><ul><li>Whether labels peeked at the future;</li><li>Whether the training set and prediction date were isolated;</li><li>Whether the backtest window is too short;</li><li>Whether the best month propped up all the returns;</li><li>Whether feature importance is just an illusion from a certain period;</li><li>Whether an improvement was just a coincidence.</li></ul><p>This article only records the competition and backtest retrospective, and does not constitute any investment advice.</p><p>If I do similar projects in the future, I will put backtest hygiene, data leakage checks, and rolling validation first. Models can become complex gradually, but once the validation method is wrong, all subsequent efforts will be led into a ditch.</p><p>The most valuable part of this time may be that I really fell into it once, and then recorded it.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/06/13/competition-review-notes/</id>
    <link href="https://luanjiahao.me/2026/06/13/competition-review-notes/"/>
    <published>2026-06-13T03:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Origin"><a href="#Origin" class="headerlink" title="Origin"></a>Origin</h2><p>For this THU-BDC2026, I actually kept three project directories: the original, archive, and v2. They look like three projects, but after going through them, a more accurate description is: this is the three-stage g]]>
    </summary>
    <title>THU-BDC2026 Retrospective: I Almost Got Fooled by a Beautiful Backtest</title>
    <updated>2026-10-28T04:22:26.313Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE/"/>
    <category term="AI" scheme="https://luanjiahao.me/tags/AI/"/>
    <category term="Agent" scheme="https://luanjiahao.me/tags/Agent/"/>
    <category term="Claude Code" scheme="https://luanjiahao.me/tags/Claude-Code/"/>
    <category term="开发效率" scheme="https://luanjiahao.me/tags/%E5%BC%80%E5%8F%91%E6%95%88%E7%8E%87/"/>
    <content>
      <![CDATA[<h2 id="Background"><a href="#Background" class="headerlink" title="Background"></a>Background</h2><p>Recently, while organizing my projects, I also dug out the application materials for the Xiaomi MIMO developer token.</p><p>These materials were originally meant to explain what I had built with AI&#x2F;Agent, but after writing them, I found they were also quite suitable as a retrospective: What exactly has AI helped me with? And where can I not hand everything over to it?</p><h2 id="Which-projects-AI-was-involved-in"><a href="#Which-projects-AI-was-involved-in" class="headerlink" title="Which projects AI was involved in"></a>Which projects AI was involved in</h2><p>Mainly two types:</p><p>The first type is complete business systems.</p><p>I used AI assistance to build a campus dormitory management system and an e-commerce platform. The former is Spring Boot + MyBatis + MySQL, and the latter is a traditional Java Web project using Servlet + JSP + DBUtils.</p><p>The second type is technical experiments.</p><p>For example, small experiments such as Spring AOP, JdbcTemplate + declarative transactions, Spring MVC interceptors, and three-layer architecture. They are not necessarily large, but they are very suitable for verifying whether a knowledge point has really been learned well.</p><h2 id="What-AI-is-best-at"><a href="#What-AI-is-best-at" class="headerlink" title="What AI is best at"></a>What AI is best at</h2><p>I feel AI is best suited for handling code that is “patterned but time-consuming.”</p><p>For example:</p><ul><li>Structured classes such as Entity, DTO, and VO.</li><li>Basic templates for Controller&#x2F;Service&#x2F;Mapper.</li><li>The first version of MyBatis dynamic SQL.</li><li>DAO layer query code.</li><li>Some repetitive forms and asynchronous request snippets on the frontend.</li><li>Filling gaps in configuration files and things like CORS and uploads.</li></ul><p>It is not that I cannot write these things by hand, but that it is easy to get fatigued. AI produces the first version first, and then I revise the business logic. It really is much faster.</p><p>My own experience is that efficiency for highly patterned code can improve by 50%-60%. A traditional e-commerce project that might previously have taken two weeks to grind through slowly can, with AI assistance, run through the complete flow in about one week.</p><h2 id="But-it-cannot-take-responsibility-for-you"><a href="#But-it-cannot-take-responsibility-for-you" class="headerlink" title="But it cannot take responsibility for you"></a>But it cannot take responsibility for you</h2><p>The biggest problem with code written by AI is that it “looks very much like it is correct.”</p><p>Sometimes it gives you a Controller that looks very smooth, and the method names, annotations, and return values all seem plausible. But when you actually run it, you discover that the parameters do not match, the SQL fields are wrong, and the names passed from the frontend are inconsistent with the names received by the backend.</p><p>So when I use AI to write projects now, I try to treat it as a copilot:</p><ul><li>Let it produce the first version.</li><li>Business rules must be confirmed by myself.</li><li>SQL must be reviewed by myself.</li><li>Security boundaries must be filled in by myself.</li><li>In the end, I must run the complete flow myself.</li></ul><p>AI can reduce repetitive labor, but it cannot take on engineering judgment for you.</p><h2 id="Impact-on-learning"><a href="#Impact-on-learning" class="headerlink" title="Impact on learning"></a>Impact on learning</h2><p>The most obvious change is that the learning cycle has become shorter.</p><p>From Java SE to Servlet, and then to Spring and Spring Boot, if I had to figure it out slowly by writing everything by hand, it might have dragged on for three months. With AI assistance, I ran through this chain in about six weeks.</p><p>But there is a prerequisite here: you cannot just copy and paste.</p><p>If you only let AI finish writing and then do not understand it yourself, then the project is not very useful even after it is done. What is truly useful is letting it generate a “first version that can run,” and then breaking it down yourself:</p><ul><li>Why is this interface layered this way?</li><li>Why is this SQL written this way?</li><li>What problem does this configuration solve?</li><li>If the requirements change, where should I change things?</li></ul><p>Only then will AI become a learning accelerator, rather than a code ghostwriter.</p><h2 id="What-I-want-to-do-next"><a href="#What-I-want-to-do-next" class="headerlink" title="What I want to do next"></a>What I want to do next</h2><p>Going forward, I want to continue applying AI to more realistic workflows.</p><p>For example:</p><ul><li>Connect a mini-program frontend to the dormitory management system.</li><li>Have AI help generate unit tests.</li><li>Do automated code review.</li><li>Try connecting Agent into the backend workflow to handle some multi-step tasks.</li></ul><p>What I feel now is that AI does not mean people no longer need to learn programming; rather, it makes “being able to ask questions, break down tasks, and accept results” more important.</p><p>Writing code is changing from “I type it line by line” to “I design, I judge, I accept.”</p><p>This is quite interesting.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/06/13/ai-agent-mimo-application/</id>
    <link href="https://luanjiahao.me/2026/06/13/ai-agent-mimo-application/"/>
    <published>2026-06-13T02:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Background"><a href="#Background" class="headerlink" title="Background"></a>Background</h2><p>Recently, while organizing my projects, I also dug out the application materials for the Xiaomi MIMO developer token.</p>
<p>These materials were originally meant to explain what I had built with AI]]>
    </summary>
    <title>Xiaomi MIMO Token Application: My Real Experience of Using AI/Agent to Write Projects</title>
    <updated>2026-10-28T04:22:26.313Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE/"/>
    <category term="Java" scheme="https://luanjiahao.me/tags/Java/"/>
    <category term="Servlet" scheme="https://luanjiahao.me/tags/Servlet/"/>
    <category term="JSP" scheme="https://luanjiahao.me/tags/JSP/"/>
    <category term="电商" scheme="https://luanjiahao.me/tags/%E7%94%B5%E5%95%86/"/>
    <content>
      <![CDATA[<h2 id="Background"><a href="#Background" class="headerlink" title="Background"></a>Background</h2><p>Writing Servlet + JSP again nowadays feels a bit retro.</p><p>The frontend world has long been full of React and Vue, and on the backend most projects start with Spring Boot. But I still think that writing an e-commerce demo by hand with Servlet and JSP is a great way to shore up Web fundamentals.</p><p>Because it doesn’t have so many frameworks hiding the details for you. How requests come in, how Session is stored, how pages navigate, how DAO queries the database—you have to write all of it yourself.</p><p>The project is here: <a href="https://github.com/yfgug/project">yfgug&#x2F;project</a></p><h2 id="Features-roughly-look-like-this"><a href="#Features-roughly-look-like-this" class="headerlink" title="Features roughly look like this"></a>Features roughly look like this</h2><p>This e-commerce platform isn’t a toy page; the basic shopping flow is complete:</p><ul><li>User module: registration, login, logout, password change, shipping address modification.</li><li>Product module: category browsing, paginated queries, keyword search, product details.</li><li>Recommendation module: scrolling banners, popular recommendations, new product recommendations.</li><li>Shopping cart: Session-based cart storage, asynchronous product addition.</li><li>Order module: cart checkout, order generation, payment method selection.</li></ul><p>Technically it uses traditional Java Web: Servlet, JSP, JSTL, Filter, DAO, Service. Database access uses Apache DBUtils; the connection pool configuration actually uses Druid, and the traditional dependency notes are also retained.</p><h2 id="Session-shopping-cart"><a href="#Session-shopping-cart" class="headerlink" title="Session shopping cart"></a>Session shopping cart</h2><p>The shopping cart part really shows the flavor of a traditional Web project.</p><p>When the user clicks “Add to Cart”, the frontend requests <code>goods_buy</code>. The backend gets the <code>Order</code> object from Session; if there isn’t one, it creates a new one, then queries stock by product id, and if stock is sufficient, adds the product to the cart.</p><p>This logic isn’t complex, but it connects several fundamentals:</p><ul><li>Session is not an abstract concept; it can really store the current user’s temporary state.</li><li>Servlet handles one request, but the shopping cart needs to persist across requests.</li><li>The backend cannot trust the frontend; stock still has to be checked before adding to the cart.</li></ul><p>Previously I only knew Session as a session. After writing this, I understand more concretely that it is the key to maintaining state in many traditional Web applications.</p><h2 id="The-DAO-layer-also-has-pitfalls"><a href="#The-DAO-layer-also-has-pitfalls" class="headerlink" title="The DAO layer also has pitfalls"></a>The DAO layer also has pitfalls</h2><p>When you don’t use an ORM, SQL stays very close to you.</p><p>DBUtils’ <code>QueryRunner</code> saves a lot of boilerplate code, and <code>BeanHandler</code> and <code>MapListHandler</code> make result set handling more comfortable. But you are still responsible for the SQL itself.</p><p>For example, with paginated queries, you can’t only think about how many rows the page displays; you also have to calculate:</p><ul><li>Which page is current.</li><li>How many rows per page.</li><li>Total record count.</li><li>Total page count.</li><li>Where <code>limit</code> starts.</li></ul><p>These things look trivial, but e-commerce list pages can’t do without them.</p><h2 id="Why-this-kind-of-project-still-matters"><a href="#Why-this-kind-of-project-still-matters" class="headerlink" title="Why this kind of project still matters"></a>Why this kind of project still matters</h2><p>After finishing this project, I understand better why Spring Boot is comfortable.</p><p>It’s not because Servlet&#x2F;JSP can’t be used, but because in traditional Java Web too many things need to be managed by you personally:</p><ul><li>Encoding filter.</li><li>Route mapping.</li><li>Session state.</li><li>JSP page navigation.</li><li>DAO hand-written SQL.</li><li>Dependency packages manually placed in <code>WEB-INF/lib</code>.</li></ul><p>After writing it once, when you go back to Spring MVC and Spring Boot, you won’t feel that those auto-configurations are magic. They just package these old tasks well.</p><h2 id="Final-thoughts"><a href="#Final-thoughts" class="headerlink" title="Final thoughts"></a>Final thoughts</h2><p>This e-commerce platform isn’t modern, but it’s very suitable for practice.</p><p>It made me realize that a Web application is not just pages, and not just a database. A real project is a complete chain: the user clicks a button, the request enters the backend, the backend handles state and data, and then returns the result to the page.</p><p>Once this chain works end to end, learning whatever framework comes next is a little easier.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/06/13/servlet-jsp-ecommerce/</id>
    <link href="https://luanjiahao.me/2026/06/13/servlet-jsp-ecommerce/"/>
    <published>2026-06-13T01:30:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Background"><a href="#Background" class="headerlink" title="Background"></a>Background</h2><p>Writing Servlet + JSP again nowadays feels a bit retro.</p>
<p>The frontend world has long been full of React and Vue, and on the backend most projects start with Spring Boot. But I still think that]]>
    </summary>
    <title>Building an E-commerce Platform with Servlet and JSP: A Bit Old-School, but It Really Trains the Fundamentals</title>
    <updated>2026-10-28T04:22:26.313Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE/"/>
    <category term="Spring Boot" scheme="https://luanjiahao.me/tags/Spring-Boot/"/>
    <category term="MyBatis" scheme="https://luanjiahao.me/tags/MyBatis/"/>
    <category term="MySQL" scheme="https://luanjiahao.me/tags/MySQL/"/>
    <category term="后端" scheme="https://luanjiahao.me/tags/%E5%90%8E%E7%AB%AF/"/>
    <content>
      <![CDATA[<h2 id="How-It-Started"><a href="#How-It-Started" class="headerlink" title="How It Started"></a>How It Started</h2><p>A campus dormitory management system sounds a lot like a course design project.</p><p>Student information, dorm numbers, repair tickets, announcement management—these features are all very common. At first I also thought it was just a CRUD project: write a few APIs, connect a database, and make the pages clickable.</p><p>But after actually finishing it, I realized that CRUD is only the outermost shell. As soon as you make the features slightly more complete, you run into many real problems.</p><p>The project is in this repository: <a href="https://github.com/yfgug/project">yfgug&#x2F;project</a></p><h2 id="Features-Implemented"><a href="#Features-Implemented" class="headerlink" title="Features Implemented"></a>Features Implemented</h2><p>The system mainly has several modules:</p><ul><li>Student management: create, read, update, delete, multi-condition search, batch deletion, avatar upload.</li><li>Repair tickets: submit, in progress, resolved, delete.</li><li>Announcement management: publish, edit, delete.</li><li>Login protection: temporarily lock the account after consecutive incorrect password attempts.</li><li>File upload: extension restriction, size limit, UUID renaming.</li></ul><p>The tech stack is Spring Boot 2.5 + MyBatis + MySQL. The backend uses a RESTful API, return values are uniformly wrapped in a <code>Result&lt;T&gt;</code> layer, configuration is placed in YAML, and the database connection pool uses HikariCP.</p><h2 id="The-First-Pitfall-Was-Querying"><a href="#The-First-Pitfall-Was-Querying" class="headerlink" title="The First Pitfall Was Querying"></a>The First Pitfall Was Querying</h2><p>The most common requirement in student management is search.</p><p>Searching by name alone is simple, and searching by dorm number alone is also simple. But real pages often use combined conditions: name, gender, dorm number, and move-in date range—users fill in whichever ones they want.</p><p>If you write a separate SQL statement for every combination, it quickly becomes a disaster.</p><p>Later I used MyBatis dynamic SQL to handle it:</p><ul><li>Add name condition if name is present.</li><li>Add gender condition if gender is present.</li><li>Add dorm number condition if dorm number is present.</li><li>Add date range condition if a date range is present.</li><li>For batch deletion, use <code>&lt;foreach&gt;</code> to expand the id list.</li></ul><p>This was the first time I felt quite intuitively that frameworks are not for showing off—they can really save you.</p><h2 id="File-Uploads-Are-More-Troublesome-Than-Expected"><a href="#File-Uploads-Are-More-Troublesome-Than-Expected" class="headerlink" title="File Uploads Are More Troublesome Than Expected"></a>File Uploads Are More Troublesome Than Expected</h2><p>Avatar upload also looked simple at first: the frontend sends a file, the backend stores it, and then writes the path into the database.</p><p>But problems soon appeared:</p><ul><li>Is the file the user uploads actually an image?</li><li>What if the file is too large?</li><li>Will two people uploading files with the same name overwrite each other?</li><li>How is the upload directory mapped to an accessible path?</li></ul><p>The final approach was:</p><ul><li>Only allow <code>jpg/jpeg/png/gif/bmp/webp</code>.</li><li>Limit size to 2MB.</li><li>Rename with UUID.</li><li>Set up separate upload configuration and resource mapping.</li></ul><p>If these details are not handled, the feature might still run. But once it is actually used by others, problems easily arise.</p><h2 id="Login-Brute-Force-Protection"><a href="#Login-Brute-Force-Protection" class="headerlink" title="Login Brute-Force Protection"></a>Login Brute-Force Protection</h2><p>This feature is not complicated, but I found it quite interesting.</p><p>I used <code>ConcurrentHashMap</code> to record the number of failed login attempts. If the same account enters an incorrect password 3 times in a row, it is locked for 5 seconds.</p><p>Of course, this is not a professional security solution, nor can it replace captchas, rate limiting, audit logs, and similar things. But for a practice project, it at least got me thinking that “an API cannot consider only the normal path.”</p><p>Previously, when writing a login API, I would only think about whether the password is correct. Now I think one step further: what if someone keeps trying passwords?</p><h2 id="How-It-Felt-After-Finishing"><a href="#How-It-Felt-After-Finishing" class="headerlink" title="How It Felt After Finishing"></a>How It Felt After Finishing</h2><p>The greatest value of this project is not how many features it has, but that it strings together several common backend tasks:</p><ul><li>Receive requests.</li><li>Validate parameters.</li><li>Call the business layer.</li><li>Access the database.</li><li>Handle files.</li><li>Return a unified result.</li><li>Make the frontend pages actually usable.</li></ul><p>Before writing it, I thought Spring Boot was just a bunch of annotations. Looking back after finishing, it is actually helping me organize these repetitive but important tasks.</p><p>The project is not large, but it gave me a clearer sense of “what a backend system should look like.”</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/06/13/dormitory-system-springboot/</id>
    <link href="https://luanjiahao.me/2026/06/13/dormitory-system-springboot/"/>
    <published>2026-06-13T01:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="How-It-Started"><a href="#How-It-Started" class="headerlink" title="How It Started"></a>How It Started</h2><p>A campus dormitory management system sounds a lot like a course design project.</p>
<p>Student information, dorm numbers, repair tickets, announcement management—these features are a]]>
    </summary>
    <title>Campus Dormitory Management System: Even a CRUD Project Can Be Full of Pitfalls</title>
    <updated>2026-10-28T04:22:26.317Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE/"/>
    <category term="Java" scheme="https://luanjiahao.me/tags/Java/"/>
    <category term="Spring" scheme="https://luanjiahao.me/tags/Spring/"/>
    <category term="项目复盘" scheme="https://luanjiahao.me/tags/%E9%A1%B9%E7%9B%AE%E5%A4%8D%E7%9B%98/"/>
    <category term="AI辅助开发" scheme="https://luanjiahao.me/tags/AI%E8%BE%85%E5%8A%A9%E5%BC%80%E5%8F%91/"/>
    <content>
      <![CDATA[<h2 id="How-It-Started"><a href="#How-It-Started" class="headerlink" title="How It Started"></a>How It Started</h2><p>When I was learning Java before, I always had a strange feeling: I could understand the syntax, and I could follow along with videos, but when it came to actually building something myself, I did not know where to start.</p><p>So later I simply put a bunch of exercises and small projects into one repository, stacking them up from Java basics, JDBC, and Servlet&#x2F;JSP all the way to Spring, Spring MVC, and Spring Boot. It looks a bit messy, but it happens to record my process from “being able to write code” to “being able to get a business system running.”</p><p>Project URL: <a href="https://github.com/yfgug/project">yfgug&#x2F;project</a></p><h2 id="What-Is-in-This-Repository"><a href="#What-Is-in-This-Repository" class="headerlink" title="What Is in This Repository"></a>What Is in This Repository</h2><p>The repository is mainly divided into four parts:</p><table><thead><tr><th>Directory</th><th>Content</th><th>Tech Stack</th></tr></thead><tbody><tr><td><code>java-basics/</code></td><td>Java basics and JDBC exercises</td><td>JDBC, PreparedStatement</td></tr><tr><td><code>ecommerce-platform/</code></td><td>A traditional e-commerce platform demo</td><td>Servlet, JSP, DBUtils, connection pool</td></tr><tr><td><code>spring-experiments/</code></td><td>A collection of Spring ecosystem experiments</td><td>Spring, Spring MVC, AOP, transactions</td></tr><tr><td><code>dormitory-system/</code></td><td>Campus dormitory management system</td><td>Spring Boot, MyBatis, MySQL</td></tr></tbody></table><p>This is not one of those projects that immediately throws in microservices, gateways, and a full message-queue stack. It is more like a learning path: first get database connectivity straight, then get web requests working end to end, then understand layering, and finally use Spring Boot to integrate all of it.</p><h2 id="Why-I-Think-It-Is-Useful"><a href="#Why-I-Think-It-Is-Useful" class="headerlink" title="Why I Think It Is Useful"></a>Why I Think It Is Useful</h2><p>Many tutorials break knowledge points into very small pieces: today JDBC, tomorrow Servlet, the day after tomorrow Spring Bean. No single concept is hard, but the real difficulty is connecting them.</p><p>For example, behind the most ordinary “query student list” there is actually a whole chain:</p><ol><li>The page sends a request.</li><li>The Controller receives parameters.</li><li>The Service handles business rules.</li><li>The Mapper&#x2F;DAO queries the database.</li><li>Data is returned in a unified format.</li><li>The frontend then renders the result.</li></ol><p>Once this chain works end to end, the annotations in Spring no longer feel so mysterious. <code>@Controller</code>, <code>@Service</code>, and <code>@Mapper</code> are not there to look fancy; they simply put different responsibilities in different places.</p><h2 id="CRUD-Is-Nothing-to-Be-Ashamed-Of"><a href="#CRUD-Is-Nothing-to-Be-Ashamed-Of" class="headerlink" title="CRUD Is Nothing to Be Ashamed Of"></a>CRUD Is Nothing to Be Ashamed Of</h2><p>I used to think CRUD projects were ordinary and not worth writing about.</p><p>Later I realized CRUD is not simple at all. Create, read, update, and delete are only the surface; the real trouble is all the edge cases:</p><ul><li>Should queries support combinations of multiple conditions?</li><li>Should deletion support batch operations?</li><li>Should file uploads restrict format and size?</li><li>Should login be locked after too many failures?</li><li>Should the data format returned to the frontend be standardized?</li><li>Is the shopping cart in the Session still there after the page is refreshed?</li></ul><p>These things do not appear in “Hello World,” but they appear in every slightly more realistic project.</p><h2 id="What-AI-Did-in-It"><a href="#What-AI-Did-in-It" class="headerlink" title="What AI Did in It"></a>What AI Did in It</h2><p>Quite a bit of the code in these projects was generated with AI assistance, especially the highly template-like parts: entity classes, Controllers, Services, Mappers, DAOs, and some frontend interaction snippets.</p><p>But I increasingly feel that AI writing code is not about “completing the project for you”; it is more like “pulling you out of repetitive work.”</p><p>It can help me quickly generate a first version, but in the end I still have to review it myself:</p><ul><li>Does the SQL query the wrong table?</li><li>Are parameters validated?</li><li>Are there security issues in file uploads?</li><li>Can the business flow actually work end to end?</li><li>Does the code merely run, or can it still be changed later?</li></ul><p>This step cannot be skipped. Otherwise, the project may look complete while being full of pitfalls inside.</p><h2 id="Final-Thoughts"><a href="#Final-Thoughts" class="headerlink" title="Final Thoughts"></a>Final Thoughts</h2><p>This repository is not advanced, but it matters a lot to me.</p><p>It confirmed one thing for me: learning backend is not about memorizing framework names or stacking tech stacks; it is about running through the relationships among data, requests, business logic, and pages again and again.</p><p>From Java SE to Spring Boot, the real change is not that the code got shorter, but that I began to know how a project should be split, connected, and delivered.</p><p>That is probably the point of practicing with projects.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/06/13/java-projects-roadmap/</id>
    <link href="https://luanjiahao.me/2026/06/13/java-projects-roadmap/"/>
    <published>2026-06-13T00:30:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="How-It-Started"><a href="#How-It-Started" class="headerlink" title="How It Started"></a>How It Started</h2><p>When I was learning Java before, I always had a strange feeling: I could understand the syntax, and I could follow along with videos, but when it came to actually building something]]>
    </summary>
    <title>After Piling Up Java Projects, I Finally Understood Spring</title>
    <updated>2026-10-28T04:22:26.321Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE/"/>
    <category term="Rust" scheme="https://luanjiahao.me/tags/Rust/"/>
    <category term="Tauri" scheme="https://luanjiahao.me/tags/Tauri/"/>
    <category term="QQ" scheme="https://luanjiahao.me/tags/QQ/"/>
    <category term="桌面应用" scheme="https://luanjiahao.me/tags/%E6%A1%8C%E9%9D%A2%E5%BA%94%E7%94%A8/"/>
    <content>
      <![CDATA[<h2 id="Background"><a href="#Background" class="headerlink" title="Background"></a>Background</h2><p>Here’s how it happened.</p><p>I have a habit of exporting and backing up my QQ chat history every so often. Not for any particular reason—just afraid that one day the account will be gone, and the chat history will be gone too.</p><p>Previously I had always used a tool called QQFlow, written in Electron + Python, and it worked. But there were two problems: first, the software was too large—the installer was 188MB; second, opening a large database was painfully slow—a 190MB database took several minutes.</p><p>One night I was waiting for it to load again, watching the progress bar not move at all, and suddenly thought: why not rewrite one in Rust?</p><h2 id="It-wasn’t-actually-that-hard"><a href="#It-wasn’t-actually-that-hard" class="headerlink" title="It wasn’t actually that hard"></a>It wasn’t actually that hard</h2><p>Saying “rewrite” somewhat overestimates me.</p><p>What I did was more like “integration.” The core idea was based on the architecture of <a href="https://github.com/hicccc77/WeFlow">WeFlow</a>, a WeChat chat history export tool—load all messages into an in-memory cache at once; subsequent queries go directly to memory and no longer touch the database.</p><p>The specific implementation was like this:</p><ol><li><p>Key extraction: inject into the QQ process using the Windows Debug API and find the SQLCipher encryption key in memory. There was ready-made code for this part online; I modified it a bit and used it.</p></li><li><p>Database decryption: use rusqlite + bundled-sqlcipher to stream-copy a decrypted copy of the database to a temporary directory. It is copied only once; afterward, the cache is read directly.</p></li><li><p>Message loading: <code>SELECT *</code> full table scan, storing all messages grouped by group number&#x2F;private chat target in a HashMap. A 190MB database takes about 30-60 seconds; after that, all queries are O(1).</p></li><li><p>Frontend: React + TypeScript + Vite; I copied styles from a few open-source projects and pieced them together.</p></li></ol><p>To be honest, I didn’t write much original code. Most of the time was spent debugging, stepping into pitfalls, and reading error logs.</p><h2 id="Pitfalls-I-encountered"><a href="#Pitfalls-I-encountered" class="headerlink" title="Pitfalls I encountered"></a>Pitfalls I encountered</h2><h3 id="Pitfall-1-Deadlock"><a href="#Pitfall-1-Deadlock" class="headerlink" title="Pitfall 1: Deadlock"></a>Pitfall 1: Deadlock</h3><p>The first version had a very strange bug: clicking group chat analysis always showed “Loading…”.</p><p>It took two days of debugging to discover it was a deadlock. <code>load_store_if_needed</code> performed database operations while holding a Mutex lock, while another thread was also waiting for that lock. The two threads waited on each other and got stuck.</p><p>The solution was to separate “checking the cache” from “performing database operations”: first check the cache, release the lock, perform the database operation, then acquire the lock again to store. Sounds simple, but it took me three revisions to stabilize it.</p><h3 id="Pitfall-2-BLOB-parsing"><a href="#Pitfall-2-BLOB-parsing" class="headerlink" title="Pitfall 2: BLOB parsing"></a>Pitfall 2: BLOB parsing</h3><p>QQ message content is stored in a BLOB field as Protobuf-encoded binary data. An early version of the parser decoded varint bytes into CJK Extension Area characters, outputting a bunch of “obscure character” garbage.</p><p>Later I added a purity filter: keep it only if more than 60% of the characters are common Chinese characters. But this threshold was too high, and English messages would be filtered out. It was only solved by first trying to extract readable text, then falling back to ASCII classification.</p><p>There was an even more absurd problem: some binary BLOBs would send the parser into an infinite loop, maxing out CPU at 100%. In the end, I added an operation budget (<code>budget = blob_size × 50</code>) and automatically fell back when the budget was exceeded, which finally fixed it.</p><h3 id="Pitfall-3-UID-mapping"><a href="#Pitfall-3-UID-mapping" class="headerlink" title="Pitfall 3: UID mapping"></a>Pitfall 3: UID mapping</h3><p>QQ senders are not stored directly as QQ numbers but as a UID (something like <code>u_-PBswiplK-7J7bmaQLA-mA</code>). The QQ number is stored in another mapping table.</p><p>The problem was that this mapping table’s column names were all numbers (<code>48901</code>, <code>48902</code>, <code>1002</code>) and had no semantics. My original logic was to find the first non-numeric column as the UID and the first numeric column as the QQ number, but the values in the first column were purely numeric sequence numbers and were misidentified as QQ numbers.</p><p>In the end, I wrote a 4-level lookup strategy: direct lookup by known column name → automatic detection of candidate tables → fuzzy table-name search → brute-force full-table scan. Only then did it become compatible with mapping tables from various QQ versions.</p><h3 id="Pitfall-4-CSV-garbled-text"><a href="#Pitfall-4-CSV-garbled-text" class="headerlink" title="Pitfall 4: CSV garbled text"></a>Pitfall 4: CSV garbled text</h3><p>When the exported CSV file was opened in Excel, all Chinese text was garbled. After investigating for quite a while, I found it was an encoding issue—UTF-8 had no BOM header, and Excel parsed it as GBK by default.</p><p>I added a 3-byte BOM (<code>\xEF\xBB\xBF</code>) at the beginning of the file, and the problem was solved. These kinds of small issues are the most annoying.</p><h2 id="Final-results"><a href="#Final-results" class="headerlink" title="Final results"></a>Final results</h2><table><thead><tr><th>Feature</th><th>Original (Electron + Python)</th><th>Rust version (Tauri)</th></tr></thead><tbody><tr><td>Installer size</td><td>~188MB</td><td>~10MB</td></tr><tr><td>Memory usage</td><td>~200MB</td><td>~30MB</td></tr><tr><td>Opening a 190MB database</td><td>Several minutes</td><td>30-60 seconds (first time), instant afterward</td></tr></tbody></table><h2 id="Feature-list"><a href="#Feature-list" class="headerlink" title="Feature list"></a>Feature list</h2><ul><li>Key extraction (Windows Debug API)</li><li>Database decryption (SQLCipher)</li><li>TXT export (sorted by time, grouped by date)</li><li>CSV export (with BOM, can be opened directly in Excel)</li><li>Chat analysis (24h distribution, type pie chart, member ranking, high-frequency phrases)</li><li>Multi-account support (independent cache per QQ number)</li><li>Dark&#x2F;light themes</li></ul><h2 id="Open-source"><a href="#Open-source" class="headerlink" title="Open source"></a>Open source</h2><p>Project address: <a href="https://github.com/yfgug/QQFlow">yfgug&#x2F;QQFlow</a></p><p>Currently it only supports Windows because key extraction uses the Windows API. On Mac and Linux, the key extraction method is different, and I haven’t looked into it yet.</p><h2 id="Final-Thoughts"><a href="#Final-Thoughts" class="headerlink" title="Final Thoughts"></a>Final Thoughts</h2><p>I worked on this project for two months from start to finish. It’s called a “rewrite,” but most of the time was actually spent debugging. Rust’s compile errors are really headache-inducing; sometimes a lifetime issue could block me for half a day.</p><p>But I still finished it in the end.</p><p>My biggest takeaway after finishing was: many times you don’t need to build wheels from scratch. See how others do it, take the good ideas, and implement them once in your own way—the process itself is learning.</p><p>Thanks to the author of WeFlow for their open-source contribution. Without their project, I probably wouldn’t have thought of using this architecture.</p><blockquote><p><em>“Code is the art of integration, not a patent of originality.”</em></p></blockquote>]]>
    </content>
    <id>https://luanjiahao.me/2026/05/20/qqflow-rust/</id>
    <link href="https://luanjiahao.me/2026/05/20/qqflow-rust/"/>
    <published>2026-05-20T10:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Background"><a href="#Background" class="headerlink" title="Background"></a>Background</h2><p>Here’s how it happened.</p>
<p>I have a habit of exporting and backing up my QQ chat history every so often. Not for any particular reason—just afraid that one day the account will be gone, and the]]>
    </summary>
    <title>Rewrote QQFlow in Rust, from 188MB to 10MB</title>
    <updated>2026-10-28T04:22:26.309Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="项目" scheme="https://luanjiahao.me/categories/%E9%A1%B9%E7%9B%AE/"/>
    <category term="Python" scheme="https://luanjiahao.me/tags/Python/"/>
    <category term="爬虫" scheme="https://luanjiahao.me/tags/%E7%88%AC%E8%99%AB/"/>
    <category term="数据分析" scheme="https://luanjiahao.me/tags/%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90/"/>
    <category term="B站" scheme="https://luanjiahao.me/tags/B%E7%AB%99/"/>
    <content>
      <![CDATA[<h2 id="Background"><a href="#Background" class="headerlink" title="Background"></a>Background</h2><p>It went like this.</p><p>One night I was browsing the Bilibili Game Center and noticed that some games had high ratings but low download counts. Others had average ratings but high download counts.</p><p>I wondered whether there were any patterns among these data points.</p><p>So I wrote this scraper.</p><h2 id="Project-Overview"><a href="#Project-Overview" class="headerlink" title="Project Overview"></a>Project Overview</h2><p>This project does something simple: it scrapes data from the Bilibili Game Center, then performs analysis and visualization.</p><p>The scraped data includes game names, genres, platforms, ratings, follower counts, download counts, ranking data, and more.</p><p>The analysis covers genre distribution, platform distribution, rating distribution, the relationship between follower counts and downloads, revenue estimation, and more.</p><h2 id="Data-Acquisition-Modes"><a href="#Data-Acquisition-Modes" class="headerlink" title="Data Acquisition Modes"></a>Data Acquisition Modes</h2><p>The project supports three data acquisition modes:</p><table><thead><tr><th>Mode</th><th>Description</th><th>Use Case</th></tr></thead><tbody><tr><td><code>demo</code></td><td>Built-in simulated data for 88 real domestic games</td><td>Development and debugging, offline demos</td></tr><tr><td><code>web</code></td><td>Parse from Bilibili Game Center HTML pages</td><td>Actual data collection</td></tr><tr><td><code>api</code></td><td>Call Bilibili API endpoints</td><td>Actual data collection</td></tr></tbody></table><p><code>demo</code> mode is for people who don’t want to scrape data and just want to see what the analysis looks like. It includes data for 88 real domestic games. Although it isn’t the latest, it’s enough for a demo.</p><p><code>web</code> mode gets data by parsing HTML pages. It doesn’t require an API key, but it’s a bit slower.</p><p><code>api</code> mode calls Bilibili’s API directly. It’s fast, but you may need to deal with anti-scraping measures.</p><h2 id="Revenue-Estimation"><a href="#Revenue-Estimation" class="headerlink" title="Revenue Estimation"></a>Revenue Estimation</h2><p>The most interesting part of this project is revenue estimation.</p><p>The Bilibili Game Center does not publish game revenue data, so I can only estimate it through other metrics. The metrics I used are follower counts and ratings.</p><p>Follower counts represent a game’s popularity, and ratings represent its quality. A game with high popularity and high quality is very likely to have decent revenue.</p><p>The estimation formula is simple: revenue &#x3D; follower count * rating coefficient * platform coefficient. This formula is not precise, but it can show the general trend.</p><h2 id="Visualization"><a href="#Visualization" class="headerlink" title="Visualization"></a>Visualization</h2><p>The project generates 7 types of charts:</p><ol><li>Game genre distribution pie chart</li><li>Platform distribution bar chart</li><li>Rating distribution histogram</li><li>Follower count vs. download count scatter plot</li><li>Estimated revenue ranking chart</li><li>Popularity ranking TOP10</li><li>Top-grossing ranking TOP10</li></ol><p>These charts are generated with Matplotlib. The styling is average, but the data is accurate. If you want better-looking charts, you can switch to ECharts or Plotly yourself.</p><h2 id="Some-Findings"><a href="#Some-Findings" class="headerlink" title="Some Findings"></a>Some Findings</h2><p>After scraping the data, I found some interesting things:</p><ol><li><p><strong>Anime-style games generally have higher ratings.</strong> This may be because anime-style game players are more willing to give high scores.</p></li><li><p><strong>For indie games, follower counts and download counts are not proportional.</strong> Some indie games have high follower counts but low download counts. This may be because players only bookmark them and don’t actually play them.</p></li><li><p><strong>Popular games in the Bilibili Game Center are not quite the same as popular games on the App Store.</strong> Bilibili users prefer anime-style, indie, and niche games.</p></li><li><p><strong>Games on the pre-registration chart perform unevenly after launch.</strong> A high pre-registration count doesn’t mean the game is good; it may just mean the marketing was good.</p></li></ol><p>These findings aren’t particularly deep insights, but I find them quite interesting.</p><h2 id="Project-Structure"><a href="#Project-Structure" class="headerlink" title="Project Structure"></a>Project Structure</h2><figure class="highlight nix"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">bgame<span class="symbol">/</span></span><br><span class="line">├── main.py                 <span class="comment"># 主入口</span></span><br><span class="line">├── bili_game_scraper<span class="symbol">/</span>      <span class="comment"># 爬虫模块</span></span><br><span class="line">│   ├── spider.py           <span class="comment"># 爬虫引擎</span></span><br><span class="line">│   ├── demo_data.py        <span class="comment"># 模拟数据生成器</span></span><br><span class="line">│   └── parsers<span class="symbol">/</span>            <span class="comment"># 解析器</span></span><br><span class="line">├── analysis<span class="symbol">/</span>               <span class="comment"># 分析模块</span></span><br><span class="line">│   ├── statistics.py       <span class="comment"># 统计分析</span></span><br><span class="line">│   └── visualization.py    <span class="comment"># 可视化</span></span><br><span class="line">├── storage<span class="symbol">/</span>                <span class="comment"># 存储模块</span></span><br><span class="line">│   └── sqlite_storage.py   <span class="comment"># SQLite 存储</span></span><br><span class="line">├── scheduler<span class="symbol">/</span>              <span class="comment"># 调度模块</span></span><br><span class="line">│   └── scheduler.py        <span class="comment"># 定时调度</span></span><br><span class="line">├── config<span class="symbol">/</span>                 <span class="comment"># 配置模块</span></span><br><span class="line">│   └── settings.py         <span class="comment"># 配置管理</span></span><br><span class="line">├── data<span class="symbol">/</span>                   <span class="comment"># 数据目录</span></span><br><span class="line">├── charts<span class="symbol">/</span>                 <span class="comment"># 图表目录</span></span><br><span class="line">└── exports<span class="symbol">/</span>                <span class="comment"># 导出目录</span></span><br></pre></td></tr></table></figure><h2 id="Quick-Start"><a href="#Quick-Start" class="headerlink" title="Quick Start"></a>Quick Start</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装依赖</span></span><br><span class="line">pip install -r requirements.txt</span><br><span class="line"></span><br><span class="line"><span class="comment"># 一键执行完整流程：爬取→分析→可视化→导出</span></span><br><span class="line">python main.py all</span><br><span class="line"></span><br><span class="line"><span class="comment"># 只爬取数据</span></span><br><span class="line">python main.py crawl --max-pages 5</span><br><span class="line"></span><br><span class="line"><span class="comment"># 只生成分析报告</span></span><br><span class="line">python main.py analyze --save</span><br><span class="line"></span><br><span class="line"><span class="comment"># 只生成可视化图表</span></span><br><span class="line">python main.py visualize</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动定时爬取</span></span><br><span class="line">python main.py schedule --interval 6</span><br></pre></td></tr></table></figure><h2 id="Pitfalls-I-Encountered"><a href="#Pitfalls-I-Encountered" class="headerlink" title="Pitfalls I Encountered"></a>Pitfalls I Encountered</h2><h3 id="Pitfall-1-Anti-Scraping"><a href="#Pitfall-1-Anti-Scraping" class="headerlink" title="Pitfall 1: Anti-Scraping"></a>Pitfall 1: Anti-Scraping</h3><p>Bilibili has anti-scraping mechanisms, and making requests too frequently can get your IP blocked. I added random delays and User-Agent rotation, but sometimes I still got blocked.</p><h3 id="Pitfall-2-Data-Format"><a href="#Pitfall-2-Data-Format" class="headerlink" title="Pitfall 2: Data Format"></a>Pitfall 2: Data Format</h3><p>The data format in the Bilibili Game Center is not very consistent. Some games have empty fields, and some games have fields in different formats. I wrote several parsers to handle these special cases.</p><h3 id="Pitfall-3-Pagination"><a href="#Pitfall-3-Pagination" class="headerlink" title="Pitfall 3: Pagination"></a>Pitfall 3: Pagination</h3><p>The pagination logic in the Bilibili Game Center is a bit strange. Sometimes requesting the first page returns duplicate data. I only solved this problem by adding deduplication logic.</p><h2 id="Open-Source"><a href="#Open-Source" class="headerlink" title="Open Source"></a>Open Source</h2><p>Project address: <a href="https://github.com/yfgug/bgame">yfgug&#x2F;bgame</a></p><p>If you’re interested in Bilibili game data, you can check it out. The code is average, but the functionality is complete.</p><h2 id="Final-Thoughts"><a href="#Final-Thoughts" class="headerlink" title="Final Thoughts"></a>Final Thoughts</h2><p>I used this project to practice Python scraping. After finishing it, I realized that scraping isn’t technically difficult; the hard part is handling all kinds of edge cases.</p><p>Anti-scraping, data formats, pagination, encoding… every step can go wrong. Writing a scraper is like a battle of wits with the website’s developers. Sometimes you win, sometimes you lose.</p><p>But the data you finally scrape can indeed reveal some interesting things.</p><p>That’s probably the charm of data.</p><blockquote><p><em>“Data is the oil of the new era, and analysis is the refinery.”</em></p></blockquote>]]>
    </content>
    <id>https://luanjiahao.me/2026/05/18/bilibili-game-scraper/</id>
    <link href="https://luanjiahao.me/2026/05/18/bilibili-game-scraper/"/>
    <published>2026-05-18T10:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Background"><a href="#Background" class="headerlink" title="Background"></a>Background</h2><p>It went like this.</p>
<p>One night I was browsing the Bilibili Game Center and noticed that some games had high ratings but low download counts. Others had average ratings but high download counts.]]>
    </summary>
    <title>I Scraped Bilibili Game Data and Found Some Interesting Things</title>
    <updated>2026-10-28T04:22:26.309Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="装备评测" scheme="https://luanjiahao.me/categories/%E8%A3%85%E5%A4%87%E8%AF%84%E6%B5%8B/"/>
    <category term="徒步鞋" scheme="https://luanjiahao.me/tags/%E5%BE%92%E6%AD%A5%E9%9E%8B/"/>
    <category term="装备推荐" scheme="https://luanjiahao.me/tags/%E8%A3%85%E5%A4%87%E6%8E%A8%E8%8D%90/"/>
    <category term="重装徒步" scheme="https://luanjiahao.me/tags/%E9%87%8D%E8%A3%85%E5%BE%92%E6%AD%A5/"/>
    <content>
      <![CDATA[<h2 id="Conclusion-First"><a href="#Conclusion-First" class="headerlink" title="Conclusion First"></a>Conclusion First</h2><p>I’ve paid a lot of tuition on buying shoes.</p><p>My first pair of hiking boots was the cheapest pair from Decathlon, 199 yuan. After one trip to Qiandao Lake, my ankle was almost ruined. The second pair was “military boots” bought on Taobao, advertised as genuine leather and waterproof, but on rainy days I could have raised fish inside them. The third pair was Kailas, and finally it was right.</p><p>So this piece is a summary after spending over 2,000 yuan stepping into pitfalls. I hope you don’t take my old path.</p><h2 id="Three-Things-to-Look-at-When-Choosing-Shoes"><a href="#Three-Things-to-Look-at-When-Choosing-Shoes" class="headerlink" title="Three Things to Look at When Choosing Shoes"></a>Three Things to Look at When Choosing Shoes</h2><p><strong>First, high-cut.</strong></p><p>For heavy-load hiking, high-cut is a must, no room for negotiation. Low-cut shoes are fine on flat roads, but carrying a 20 kg pack on scree slopes, with no ankle support, a sprain can happen in minutes. That time I wore low-cut shoes to Qiandao Lake, I twisted my ankle on the descent and it hurt for two weeks.</p><p><strong>Second, the sole must be stiff.</strong></p><p>Soft-soled shoes feel comfortable when walking, but after a long time the soles of your feet will hurt. Heavy-load hiking requires a sole with a certain stiffness to support your feet. A Vibram outsole is standard; don’t skimp on this.</p><p><strong>Third, waterproof.</strong></p><p>Mountain weather changes at a moment’s notice; it can be sunny in the morning and rain in the afternoon. With non-waterproof shoes on wet roads, your feet soak in water. Blisters are the mild outcome; fungal infection is the real trouble. Gore-Tex or other waterproof breathable fabrics are a must.</p><h2 id="My-Recommendations"><a href="#My-Recommendations" class="headerlink" title="My Recommendations"></a>My Recommendations</h2><h3 id="Kailas-Fuga-EX"><a href="#Kailas-Fuga-EX" class="headerlink" title="Kailas Fuga EX"></a>Kailas Fuga EX</h3><p>I’ve worn this pair for over half a year, hiked South Taihang and Wugong Mountain, and it’s currently the pair I’m most satisfied with.</p><p>The advantage is that the anti-slip is really good. Vibram Megagrip outsole, it doesn’t slip even on wet rocks. High-cut design, the ankle is wrapped very tightly, giving a strong sense of security on scree slopes. Breathability is also okay; it doesn’t make my feet stuffy in summer.</p><p>The disadvantage is that the sole is on the stiff side and needs break-in when new. The first time I wore it for 10 km, the soles of my feet hurt a little. But after wearing it a few times it was fine; the sole gradually softens according to your foot shape.</p><p>Price 400-500 yuan, very high cost-performance. Domestic shoes can reach this level, and I’m impressed.</p><h3 id="Decathlon-MH500"><a href="#Decathlon-MH500" class="headerlink" title="Decathlon MH500"></a>Decathlon MH500</h3><p>If you’re a beginner doing easy routes, this pair is enough.</p><p>Decathlon’s advantage is high comfort; you can wear it right out of the box without break-in. Waterproof performance is also decent; I’ve worn it through several rainy days and my feet stayed dry.</p><p>But for heavy loads, support is a bit lacking. I wore it on South Taihang, and after the hike my ankles were a bit sore. Maybe my ankles are weak, but Kailas on the same route had no such problem.</p><p>Price 300-400 yuan, suitable for light hiking and entry-level heavy-load hiking.</p><h3 id="Camel-Kunlun-Mountain-Series"><a href="#Camel-Kunlun-Mountain-Series" class="headerlink" title="Camel Kunlun Mountain Series"></a>Camel Kunlun Mountain Series</h3><p>If your budget is tight, you can consider this pair. You can get waterproof high-cut shoes for over 200 yuan, and the cost-performance is indeed high.</p><p>But quality control is not very stable. A friend of mine bought a pair, and the sole delaminated after three months. My other Camel pair lasted over half a year and was still fine; it depends on luck.</p><p>Anti-slip is average; be careful on wet rocks. Once I wore it through a stream valley and almost slipped into the water.</p><p>Suitable for friends who hike occasionally and have a limited budget.</p><h2 id="Socks-Are-Also-Important"><a href="#Socks-Are-Also-Important" class="headerlink" title="Socks Are Also Important"></a>Socks Are Also Important</h2><p>Good hiking boots need to be paired with good socks; don’t wear cotton socks. Cotton socks don’t dry after getting wet, and feet soaking inside will blister.</p><p>Wool socks are recommended: breathable, moisture-wicking, and your feet stay dry all day. Smartwool is a bit expensive but really good. Decathlon’s SH500 is also okay, half the price, and the quality is acceptable.</p><h2 id="Pitfalls-When-Buying-Shoes"><a href="#Pitfalls-When-Buying-Shoes" class="headerlink" title="Pitfalls When Buying Shoes"></a>Pitfalls When Buying Shoes</h2><p>Finally, a few pitfalls I’ve stepped into:</p><ol><li><p><strong>Don’t buy online.</strong> Hiking boots must be tried on; everyone’s foot shape is different. If you buy online and they don’t fit, returns and exchanges are troublesome.</p></li><li><p><strong>Don’t buy too cheap.</strong> Hiking boots under 100 yuan have soles as soft as slippers and can’t go far.</p></li><li><p><strong>Don’t buy too good-looking.</strong> Hiking boots are for walking, not for photos. Shoes that look good but aren’t practical end up gathering dust.</p></li><li><p><strong>Break them in ahead of time.</strong> After buying new shoes, wear them on a few short trips first; don’t go straight to long routes. The first time I wore new shoes on Wugong Mountain, I almost couldn’t make it back.</p></li></ol><h2 id="Final-Words"><a href="#Final-Words" class="headerlink" title="Final Words"></a>Final Words</h2><p>Shoes are the foundation of hiking. Choose the right shoes, and the road goes far.</p><p>But in the end, shoes are just tools. What really matters is whether you’re willing to take that step.</p><blockquote><p><em>“The road is under your feet, the shoes are on your feet, and the distant place is in your heart.”</em></p></blockquote>]]>
    </content>
    <id>https://luanjiahao.me/2026/05/15/hiking-gear-recommendations/</id>
    <link href="https://luanjiahao.me/2026/05/15/hiking-gear-recommendations/"/>
    <published>2026-05-15T04:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Conclusion-First"><a href="#Conclusion-First" class="headerlink" title="Conclusion First"></a>Conclusion First</h2><p>I’ve paid a lot of tuition on buying shoes.</p>
<p>My first pair of hiking boots was the cheapest pair from Decathlon, 199 yuan. After one trip to Qiandao Lake, my ankle was]]>
    </summary>
    <title>What Can You Get in a Hiking Boot for 500 Yuan? All the Pitfalls I've Stepped Into Are Here</title>
    <updated>2026-10-28T04:22:26.309Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="徒步攻略" scheme="https://luanjiahao.me/categories/%E5%BE%92%E6%AD%A5%E6%94%BB%E7%95%A5/"/>
    <category term="华东K2" scheme="https://luanjiahao.me/tags/%E5%8D%8E%E4%B8%9CK2/"/>
    <category term="九华山" scheme="https://luanjiahao.me/tags/%E4%B9%9D%E5%8D%8E%E5%B1%B1/"/>
    <category term="难度对比" scheme="https://luanjiahao.me/tags/%E9%9A%BE%E5%BA%A6%E5%AF%B9%E6%AF%94/"/>
    <category term="徒步路线" scheme="https://luanjiahao.me/tags/%E5%BE%92%E6%AD%A5%E8%B7%AF%E7%BA%BF/"/>
    <content>
      <![CDATA[<h2 id="Conclusion-First"><a href="#Conclusion-First" class="headerlink" title="Conclusion First"></a>Conclusion First</h2><p>East China K2 is much harder. They are not on the same level.</p><p>The Jiuhua Mountain north-south traverse is an intermediate-to-advanced route. After finishing it, you will feel tired, but it is still acceptable. East China K2 is a top-tier sufferfest. After finishing it, you will question life and wonder why you came to endure this punishment.</p><p>But some people just like that feeling of suffering. For example, me.</p><h2 id="Detailed-Comparison"><a href="#Detailed-Comparison" class="headerlink" title="Detailed Comparison"></a>Detailed Comparison</h2><table><thead><tr><th>Dimension</th><th>East China K2</th><th>Jiuhua Mountain North-South Traverse</th></tr></thead><tbody><tr><td><strong>Distance</strong></td><td>35-42km</td><td>25-30km</td></tr><tr><td><strong>Cumulative ascent</strong></td><td>2500-3000m+</td><td>1500-2000m</td></tr><tr><td><strong>Time required</strong></td><td>2 days (1.5 days for fast hikers)</td><td>2 days</td></tr><tr><td><strong>Trail conditions</strong></td><td>60% off-trail + bushwhacking + scrambling</td><td>70% established ancient trails</td></tr><tr><td><strong>Water sources</strong></td><td>Extremely scarce; only 2-3 spots along the entire route</td><td>Many temples; ample water</td></tr><tr><td><strong>Bailout options</strong></td><td>Almost no retreat options</td><td>Multiple bailout points</td></tr><tr><td><strong>Signal</strong></td><td>Mostly none</td><td>Mostly available</td></tr></tbody></table><h2 id="Why-East-China-K2-Is-Hard"><a href="#Why-East-China-K2-Is-Hard" class="headerlink" title="Why East China K2 Is Hard"></a>Why East China K2 Is Hard</h2><h3 id="Water-Is-the-Lifeline"><a href="#Water-Is-the-Lifeline" class="headerlink" title="Water Is the Lifeline"></a>Water Is the Lifeline</h3><p>The most brutal thing about K2 is water.</p><p>Along the entire route, only the area near Guofeng’ao has a reliable water source. Other places either have no water, or have mountain streams that you need luck to find. Each person should start with at least 3-4 liters of water, and in summer you may need to carry more.</p><p>When I hiked K2, I carried 4 liters of water, and by the afternoon of the second day it was still gone. I relied on willpower to push through the last two kilometers to the finish, and my lips were cracked.</p><h3 id="Bushwhacking-Until-You-Question-Your-Life"><a href="#Bushwhacking-Until-You-Question-Your-Life" class="headerlink" title="Bushwhacking Until You Question Your Life"></a>Bushwhacking Until You Question Your Life</h3><p>The section from Dushujian to Guofeng’ao is the most breakdown-inducing part of the entire route.</p><p>You are not walking; you are forcing your way through. Dense forest plus arrow bamboo will scrape off everything hanging on the outside of your pack. My rain cover, trekking poles, and water bottle hanging on my backpack were all gone after one kilometer. I went back to look for them and could not find them at all; they had been scraped by branches to who knows where.</p><p>And there is no trail at all in that kind of dense forest. You can only follow the person in front. Wherever the person in front goes, you go. Sometimes the person in front goes the wrong way, you follow and go the wrong way too, and then a whole group curses and grumbles while walking back.</p><h3 id="No-Way-Back"><a href="#No-Way-Back" class="headerlink" title="No Way Back"></a>No Way Back</h3><p>Once you enter the mountains on K2, you can only finish.</p><p>Unlike Wugong Mountain, where you can bail out to a guesthouse at any time, K2 has almost no bailout points. You have walked ten kilometers and want to retreat—sorry, there is no trail down the mountain, you can only keep going forward.</p><p>That feeling is terrifying. You think you can give up at any time, but reality tells you that you cannot. You can only grit your teeth and keep moving forward, until your legs go weak, until you want to cry, but you still have to keep going.</p><h3 id="The-Route-Closest-to-a-“Wild-Through-Hike”-in-East-China"><a href="#The-Route-Closest-to-a-“Wild-Through-Hike”-in-East-China" class="headerlink" title="The Route Closest to a “Wild Through-Hike” in East China"></a>The Route Closest to a “Wild Through-Hike” in East China</h3><p>K2 is not a scenic area. There are no steps, no resupply, and no rescue response.</p><p>Every step you take is on an off-trail route, and every rock you step on is wild. There are no guardrails, no signs, no toilets. If something happens to you, even if you call for rescue, there may not necessarily be anyone to come.</p><p>This is what makes K2 terrifying. It is not a built scenic-area boardwalk; it is true wilderness.</p><h2 id="Where-Jiuhua-Mountain-Is-Hard"><a href="#Where-Jiuhua-Mountain-Is-Hard" class="headerlink" title="Where Jiuhua Mountain Is Hard"></a>Where Jiuhua Mountain Is Hard</h2><h3 id="Concentrated-Ascent"><a href="#Concentrated-Ascent" class="headerlink" title="Concentrated Ascent"></a>Concentrated Ascent</h3><p>Jiuhua Mountain’s ascent is relatively concentrated. The Tiantai Peak section has more than 800 m of elevation gain. It is very tiring, but at least you know that once you finish climbing, you have arrived.</p><p>K2’s ascent is dispersed. You climb one peak and think you are there, but there is another one ahead, and another one beyond that. The psychological blow is more breakdown-inducing than the physical exertion.</p><h3 id="Stone-Slab-Paths-Destroy-Your-Knees"><a href="#Stone-Slab-Paths-Destroy-Your-Knees" class="headerlink" title="Stone Slab Paths Destroy Your Knees"></a>Stone Slab Paths Destroy Your Knees</h3><p>Jiuhua Mountain’s ancient trails are bluestone slab paths, and they are extremely slippery after rain. It rained when I hiked, and I nearly slipped several times.</p><p>But at least there is a trail. Some sections of K2 have no trail at all, and you have to find the route yourself.</p><h3 id="Relatively-Friendly"><a href="#Relatively-Friendly" class="headerlink" title="Relatively Friendly"></a>Relatively Friendly</h3><p>Jiuhua Mountain has temples where you can refill water, signposts you can refer to, and multiple bailout points. If you cannot walk anymore, you can find a temple to rest; if you are thirsty, you can ask the monks for water.</p><p>K2 has none of that. You carry your own water, carry your own food, find your own route, and bear all the risks yourself.</p><h2 id="Difficulty-Ratings"><a href="#Difficulty-Ratings" class="headerlink" title="Difficulty Ratings"></a>Difficulty Ratings</h2><table><thead><tr><th>Metric</th><th>East China K2</th><th>Jiuhua Mountain</th><th>Wugong Mountain Shenming Line</th></tr></thead><tbody><tr><td>Physical demand</td><td>9</td><td>6</td><td>5</td></tr><tr><td>Technical difficulty</td><td>8</td><td>4</td><td>3</td></tr><tr><td>Psychological pressure</td><td>9</td><td>4</td><td>3</td></tr><tr><td><strong>Overall difficulty</strong></td><td><strong>8.5</strong></td><td><strong>5</strong></td><td><strong>4</strong></td></tr></tbody></table><h2 id="Suggested-Progression-Routes"><a href="#Suggested-Progression-Routes" class="headerlink" title="Suggested Progression Routes"></a>Suggested Progression Routes</h2><p>If you want to challenge East China K2, it is recommended that you first complete these:</p><p><strong>Beginner</strong></p><ul><li>Wugong Mountain Shenming Line</li><li>Qiandao Lake Loop</li><li>Huangshan Traverse</li></ul><p><strong>Intermediate</strong></p><ul><li>Jiuhua Mountain North-South Traverse</li><li>South Taihang</li><li>Dabie Mountain Main Peak</li></ul><p><strong>Advanced</strong></p><ul><li>East China K2</li><li>Aotai Traverse</li><li>Langta C Route</li></ul><h2 id="Notes-for-K2"><a href="#Notes-for-K2" class="headerlink" title="Notes for K2"></a>Notes for K2</h2><p>If you decide to hike K2, you must know these things:</p><ol><li><p><strong>You must go in a group</strong>. At least 2-3 people; do not go alone. If something happens, no one will help you.</p></li><li><p><strong>Bring enough water</strong>. At least 3-4 liters per person, and more in summer.</p></li><li><p><strong>Download a GPX track</strong>. An offline map is essential; rely on it when there is no signal.</p></li><li><p><strong>Choose the right season</strong>. Avoid the peak snake and insect season from June to September; spring and autumn are best.</p></li><li><p><strong>Go only after you have experience</strong>. You should have hiked at least 2-3 intermediate routes and have a clear understanding of your own fitness.</p></li></ol><h2 id="My-Choice"><a href="#My-Choice" class="headerlink" title="My Choice"></a>My Choice</h2><p>I have completed South Taihang and the Shenming Line, and my next plan is the Jiuhua Mountain North-South Traverse.</p><p>I want to hike K2, but I am not in a hurry. I will challenge it after I have richer experience and better fitness. After all, the mountain will always be there, but you only have one life.</p><p>Before hiking K2, first make sure you can come back alive.</p><blockquote><p><em>“Mountains have grades, people have stages; only by progressing step by step can you go farther.”</em></p></blockquote>]]>
    </content>
    <id>https://luanjiahao.me/2026/05/11/east-china-k2-vs-jiuhuashan/</id>
    <link href="https://luanjiahao.me/2026/05/11/east-china-k2-vs-jiuhuashan/"/>
    <published>2026-05-11T14:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Conclusion-First"><a href="#Conclusion-First" class="headerlink" title="Conclusion First"></a>Conclusion First</h2><p>East China K2 is much harder. They are not on the same level.</p>
<p>The Jiuhua Mountain north-south traverse is an intermediate-to-advanced route. After finishing it, you wi]]>
    </summary>
    <title>East China K2 vs. Jiuhua Mountain: Which Is Actually Harder?</title>
    <updated>2026-10-28T04:22:26.321Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="徒步游记" scheme="https://luanjiahao.me/categories/%E5%BE%92%E6%AD%A5%E6%B8%B8%E8%AE%B0/"/>
    <category term="重装徒步" scheme="https://luanjiahao.me/tags/%E9%87%8D%E8%A3%85%E5%BE%92%E6%AD%A5/"/>
    <category term="武功山" scheme="https://luanjiahao.me/tags/%E6%AD%A6%E5%8A%9F%E5%B1%B1/"/>
    <category term="沈明线" scheme="https://luanjiahao.me/tags/%E6%B2%88%E6%98%8E%E7%BA%BF/"/>
    <category term="江西" scheme="https://luanjiahao.me/tags/%E6%B1%9F%E8%A5%BF/"/>
    <content>
      <![CDATA[<h2 id="Why-Wugong-Mountain"><a href="#Why-Wugong-Mountain" class="headerlink" title="Why Wugong Mountain"></a>Why Wugong Mountain</h2><p>After finishing the Southern Taihang, I said I wouldn’t carry a backpack for three months. In the end, I broke my resolve after two months.</p><p>It started when I came across a video of Wugong Mountain: clouds churning over alpine meadows, set to that kind of very sentimental music. I watched it three times, then quietly opened 12306.</p><p>The Shenming Line is Wugong Mountain’s most classic through-hike route. It starts from Shenzi Village, passes Golden Summit and Fayunjie, and ends at Mingyue Mountain. Three days and two nights, about 25 kilometers. Sounds like not much, right? But add 1,500 meters of cumulative ascent and a 20-kilogram backpack, and it’s a completely different matter.</p><h2 id="Shenzi-Village-to-Golden-Summit"><a href="#Shenzi-Village-to-Golden-Summit" class="headerlink" title="Shenzi Village to Golden Summit"></a>Shenzi Village to Golden Summit</h2><p>We didn’t start into the mountains until three in the afternoon. That timing was actually pretty stupid.</p><p>Normal people set off in the morning. Because our high-speed train was delayed and the chartered car got stuck in traffic, it was already afternoon when we reached Shenzi Village. My teammate said, “How about we go tomorrow?” I said, “We’re already here, let’s go.”</p><p>Those four words—“We’re already here”—are deadly.</p><p>The first half wasn’t bad: we climbed up along a stream valley, shaded by trees and not too exposed to the sun. But the higher we went, the steeper it got, and the sun began to sink. I looked at the map: still one third of the route left, and it was almost dark.</p><p>I basically climbed the last section in the dark. My headlamp lit the stone-slab path underfoot; on both sides were pitch-black woods, with occasional calls of unknown birds. To be honest, it was a bit scary, but there was no way back.</p><p>At a little after 8 p.m., I finally saw the lights of Golden Summit. At that moment my legs went weak—not from exhaustion, but from relief.</p><h2 id="Sunrise-at-Golden-Summit"><a href="#Sunrise-at-Golden-Summit" class="headerlink" title="Sunrise at Golden Summit"></a>Sunrise at Golden Summit</h2><p>The next morning I was woken by the cold at five.</p><p>The wind outside the tent was strong. I wrapped myself in the sleeping bag and crawled out; the horizon was already turning white. Quite a few people were already standing on Golden Summit, all holding up phones and waiting.</p><p>I didn’t take many photos of the sunrise. Because at that moment you realize your phone simply can’t capture what your eyes see. The sea of clouds churned below my feet; the sun rose bit by bit from the clouds, dyeing the whole sky orange-red. Distant peaks appeared and disappeared in the sea of clouds, like an ink-wash painting.</p><p>I stood there, the wind stinging my face, but I didn’t want to leave.</p><p>A woman next to me was on the phone, loud: “Let me tell you, it’s so beautiful, come see it quickly!” After hanging up she called again: “Mom, I’m doing fine here, the scenery is really beautiful.”</p><p>I listened nearby and suddenly felt a bit homesick.</p><h2 id="Alpine-Meadows"><a href="#Alpine-Meadows" class="headerlink" title="Alpine Meadows"></a>Alpine Meadows</h2><p>From Golden Summit to Fayunjie is the most beautiful section of the whole route.</p><p>Wugong Mountain’s meadows—how should I put it—are that kind of green that, when you see it in a photo, you’d think was Photoshopped. The whole mountain is green; no trees, just grass, spreading all the way to the horizon. When the wind blows, waves of grass roll one after another, like the sea.</p><p>Walking on the meadow, the ground feels soft, much more comfortable than concrete. But the sun was fierce, with no shade, and after a while I was covered in sweat. I took off my quick-dry shirt and tied it around my waist, walking shirtless; a teammate took a photo, and it later became a meme circulating in the group chat.</p><p>At noon I found a wind-sheltered spot for lunch: compressed biscuits and pickled mustard tuber, with hot water from the thermos. To be honest it wasn’t tasty, but in that environment everything tastes good.</p><h2 id="The-Starry-Sky-at-Fayunjie"><a href="#The-Starry-Sky-at-Fayunjie" class="headerlink" title="The Starry Sky at Fayunjie"></a>The Starry Sky at Fayunjie</h2><p>That night we camped at Fayunjie.</p><p>This place is a pass, with strong wind; it took three attempts to pitch the tent. At night I cooked instant noodles and added a sausage, which counted as improving the food.</p><p>The starry sky at Fayunjie was even better than at Baodu Village. Because of the high altitude and clean air, the stars seemed to hang right overhead. I lay on the sleeping pad and looked for a long time.</p><p>I saw four or five shooting stars; I forgot what I wished for. Probably some vulgar wish like getting rich.</p><p>That night I slept especially well—maybe from exhaustion, maybe because of the high altitude and good air. Anyway, I slept until dawn, and the dew outside the tent had frozen into ice.</p><h2 id="The-Steps-of-Mingyue-Mountain"><a href="#The-Steps-of-Mingyue-Mountain" class="headerlink" title="The Steps of Mingyue Mountain"></a>The Steps of Mingyue Mountain</h2><p>The third day was the descent.</p><p>From Fayunjie to Mingyue Mountain, the road wasn’t hard to walk, but there were many steps. Wugong Mountain’s steps are the standard scenic-area kind, built neatly one level after another. For heavy-load hiking, these steps are actually harder on the knees than wild trails.</p><p>By the later part, every step down hurt my knees. I started using a zigzag walking method; slower, but my knees felt better. My teammate laughed behind me, saying I looked like a crab.</p><p>At noon twelve I reached the entrance of Mingyue Mountain, my legs were shaking so much I couldn’t stand steadily. There were people selling water at the entrance; I bought a bottle of iced cola, drank it in one go, burped, and felt it was the best cola I’d ever drunk in my life.</p><h2 id="Cost-List"><a href="#Cost-List" class="headerlink" title="Cost List"></a>Cost List</h2><p>I’m writing this as a reference for people who want to hike it:</p><table><thead><tr><th>Item</th><th>Cost</th></tr></thead><tbody><tr><td>Round-trip high-speed rail</td><td>about 200 yuan</td></tr><tr><td>Chartered car</td><td>about 100 yuan</td></tr><tr><td>Food</td><td>about 100 yuan</td></tr><tr><td>Gas canister</td><td>about 25 yuan</td></tr><tr><td><strong>Total</strong></td><td><strong>about 425 yuan</strong></td></tr></tbody></table><p>Not expensive, right? But you have to pay with stamina and knees.</p><h2 id="Advice-for-Future-Hikers"><a href="#Advice-for-Future-Hikers" class="headerlink" title="Advice for Future Hikers"></a>Advice for Future Hikers</h2><ol><li><strong>Start early</strong>. Don’t follow our example and enter the mountain at 3 p.m.; hiking in the dark is not fun.</li><li><strong>Bring enough water</strong>. There is water for sale at Golden Summit, but it’s expensive. Bring 3–4 liters yourself.</li><li><strong>Sun protection</strong>. There’s no shade on the meadow; if you don’t apply sunscreen you’ll peel.</li><li><strong>Knee braces</strong>. That section of steps downhill is really killer.</li><li><strong>Bring a thick layer</strong>. It’s cold at Golden Summit at night; even going in May you’ll need a fleece.</li></ol><h2 id="Afterword"><a href="#Afterword" class="headerlink" title="Afterword"></a>Afterword</h2><p>After returning from Wugong Mountain, I rested for a week. Every day I went to work, got off work, went home and lay down, staring blankly at the ceiling.</p><p>But my mind kept thinking about those meadows, those seas of clouds, those starry skies.</p><p>Some people say Wugong Mountain is the ceiling of hiking in East China; I don’t think so. A ceiling means you’ve reached the top, but every time I finish a route, I find there are still farther places I want to go.</p><p>Maybe this is the magic of hiking. You think you’re looking at scenery, but actually you’re looking at yourself.</p><blockquote><p><em>“If the mountain won’t come to me, I’ll go to the mountain.”</em></p></blockquote><h2 id="Related-Albums-and-Footprints"><a href="#Related-Albums-and-Footprints" class="headerlink" title="Related Albums and Footprints"></a>Related Albums and Footprints</h2><p>The photos from this route are collected in the <a href="/gallery/wugongshan-shenming/">Wugong Mountain Shenming Line album</a>. You can also browse the <a href="/gallery/wugongshan-scenic/">Wugong Mountain Scenic Area album</a>, or continue to find other destinations from the <a href="/map/">footprint map</a>.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/04/26/wugongshan-shenming-line/</id>
    <link href="https://luanjiahao.me/2026/04/26/wugongshan-shenming-line/"/>
    <published>2026-04-26T10:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Why-Wugong-Mountain"><a href="#Why-Wugong-Mountain" class="headerlink" title="Why Wugong Mountain"></a>Why Wugong Mountain</h2><p>After finishing the Southern Taihang, I said I wouldn’t carry a backpack for three months. In the end, I broke my resolve after two months.</p>
<p>It started when]]>
    </summary>
    <title>Wugong Mountain Shenming Line - Three Days Above the Sea of Clouds</title>
    <updated>2026-10-28T04:22:26.321Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="徒步游记" scheme="https://luanjiahao.me/categories/%E5%BE%92%E6%AD%A5%E6%B8%B8%E8%AE%B0/"/>
    <category term="徒步" scheme="https://luanjiahao.me/tags/%E5%BE%92%E6%AD%A5/"/>
    <category term="随笔" scheme="https://luanjiahao.me/tags/%E9%9A%8F%E7%AC%94/"/>
    <category term="雨天" scheme="https://luanjiahao.me/tags/%E9%9B%A8%E5%A4%A9/"/>
    <content>
      <![CDATA[<h2 id="A-Weekend-Alone"><a href="#A-Weekend-Alone" class="headerlink" title="A Weekend Alone"></a>A Weekend Alone</h2><p>I woke up on Saturday morning, glanced at my phone, and saw it was 8:30.</p><p>It was raining outside—not the kind of heavy downpour that comes in sheets, but a fine, dense drizzle that seems to go on forever once it starts. I lay in bed staring at the ceiling, wondering what to do today.</p><p>Work overtime? No. Play games? Boring. Scroll short videos? An hour later, I felt even emptier.</p><p>In the end, I made a decision that probably wasn’t very sensible: go climb a mountain.</p><h2 id="Why-a-Rainy-Day"><a href="#Why-a-Rainy-Day" class="headerlink" title="Why a Rainy Day"></a>Why a Rainy Day</h2><p>To be honest, hiking in the rain is not a good idea.</p><p>The trails are slippery, visibility is poor, it’s easy to get hypothermia, and your phone might have no signal. Any outdoor blogger will tell you not to go into the mountains on a rainy day.</p><p>But I just wanted to get out.</p><p>Maybe it was because I’d worked overtime for three weeks straight, maybe it was because I’d had a fight with a client yesterday, or maybe it was simply because staying at home felt too stifling. Anyway, I put on my hardshell jacket, picked up my backpack, and left.</p><h2 id="Rain-in-the-Mountains"><a href="#Rain-in-the-Mountains" class="headerlink" title="Rain in the Mountains"></a>Rain in the Mountains</h2><p>I drove to the foot of the mountain. The rain was still falling.</p><p>Mine was the only car in the parking lot. I checked the weather forecast: it would stop at 3 p.m. It was 10 a.m. now, so there were still five hours to go. I had brought enough water and food, my hardshell jacket was waterproof, and my backpack had a rain cover. It should be fine.</p><p>After I entered the mountains, the sound of the rain changed. On the leaves it was a rustling sound, on the rocks a tapping sound, and on the hood of my hardshell jacket a thumping sound. Mixed together, these sounds actually made things feel quiet.</p><p>It was real quiet. Not the quiet of an office, where there’s the hum of air conditioning, the clatter of keyboards, and colleagues talking softly on the phone. The quiet here was truly nothing—only the sound of rain and your own breathing.</p><h2 id="The-Stream"><a href="#The-Stream" class="headerlink" title="The Stream"></a>The Stream</h2><p>A stream on a rainy day looks better than usual.</p><p>The water volume was larger, it flowed faster, and the sound of it hitting the rocks was louder. I walked upstream along the stream, watching the murky water surge out of the mountains, carrying leaves and branches.</p><p>In one place the stream had overflowed onto the path, and I had to step on stones to get across. The stones were covered in moss and were incredibly slippery. I stepped on them carefully, but my foot still slipped, and I almost fell into the water. In the end I caught myself with my trekking pole, but half my pant leg got wet.</p><p>Standing by the stream, watching the water rush past my feet, I suddenly found it a little funny. I’m a programmer, and on the weekend I don’t play games or sleep in—I go into the mountains to get rained on. What for?</p><p>But I just felt happy. A kind of happiness I couldn’t explain.</p><h2 id="Fog"><a href="#Fog" class="headerlink" title="Fog"></a>Fog</h2><p>When I reached the halfway point, fog set in.</p><p>The fog was thick, with visibility of less than ten meters. I couldn’t see the trees on either side of the path clearly—only the ground beneath my feet and a few meters of mountain path ahead. The whole world turned grayish-white, as if something had swallowed it up.</p><p>I stopped, stood in the fog, and listened to the rain.</p><p>At that moment I suddenly had a strange feeling, as if I were the only person left in the world. It wasn’t loneliness—it was a very calm feeling. It was as if all my worries, all my stress, all those messy things had been swallowed by the fog.</p><p>I stood there, not knowing how long. Maybe five minutes, maybe ten. It wasn’t until a gust of wind blew over and made me shiver from the cold that I kept going.</p><h2 id="The-Summit"><a href="#The-Summit" class="headerlink" title="The Summit"></a>The Summit</h2><p>When I reached the summit, the fog was even thicker.</p><p>The forecast had said the rain would stop at 3 p.m., but by 2 p.m. it was still raining. I found a wind-sheltered spot, sat on a rock, and ate lunch. Compressed biscuits, ham sausage, and hot water from my thermos. It wasn’t tasty, but in that environment, everything tasted good.</p><p>There was no view from the summit—just fog. But I didn’t care. I had come out just to walk around, not to see any scenery.</p><p>After lunch, I decided to go down. Not because I was tired, but because I suddenly remembered there was still a feature going live tomorrow, and I needed to get back and check the code.</p><h2 id="Going-Down"><a href="#Going-Down" class="headerlink" title="Going Down"></a>Going Down</h2><p>On the way down, the rain let up.</p><p>By the time I reached the foot of the mountain, the rain had stopped, and the sun was breaking through the clouds. I turned and looked back at the mountain. The fog was dispersing, revealing the green slopes.</p><p>There were a few more cars in the parking lot—probably people who had come to hike in the afternoon. I changed into dry clothes and drove home.</p><p>On the way, I turned on my phone and saw more than 200 WeChat messages. The work group was discussing tomorrow’s feature, and someone on Moments was posting about weekend afternoon tea. I turned off my phone and kept driving.</p><h2 id="Final-Thoughts"><a href="#Final-Thoughts" class="headerlink" title="Final Thoughts"></a>Final Thoughts</h2><p>That night, I slept especially well.</p><p>Not because I was tired, but because my mind was clean. Those annoying things were still there—work still had to be done, clients still had to be dealt with. But somehow they didn’t seem as annoying anymore.</p><p>Maybe that’s the meaning of hiking. It’s not about conquering some mountain, and it’s not about posting on Moments. It’s about finding a place where you can let your mind be quiet for a while.</p><p>A mountain on a rainy day has no people, no scenery, but it has quiet.</p><p>Sometimes, quiet is enough.</p><blockquote><p><em>“A mountain doesn’t have to be beautiful only on a sunny day, just as life doesn’t have to be perfect to be worth living.”</em></p></blockquote>]]>
    </content>
    <id>https://luanjiahao.me/2026/04/12/weekend-mountain-rain/</id>
    <link href="https://luanjiahao.me/2026/04/12/weekend-mountain-rain/"/>
    <published>2026-04-12T12:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="A-Weekend-Alone"><a href="#A-Weekend-Alone" class="headerlink" title="A Weekend Alone"></a>A Weekend Alone</h2><p>I woke up on Saturday morning, glanced at my phone, and saw it was 8:30.</p>
<p>It was raining outside—not the kind of heavy downpour that comes in sheets, but a fine, dense driz]]>
    </summary>
    <title>That Rain, That Mountain</title>
    <updated>2026-10-28T04:22:26.321Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="徒步游记" scheme="https://luanjiahao.me/categories/%E5%BE%92%E6%AD%A5%E6%B8%B8%E8%AE%B0/"/>
    <category term="重装徒步" scheme="https://luanjiahao.me/tags/%E9%87%8D%E8%A3%85%E5%BE%92%E6%AD%A5/"/>
    <category term="南太行" scheme="https://luanjiahao.me/tags/%E5%8D%97%E5%A4%AA%E8%A1%8C/"/>
    <category term="河南" scheme="https://luanjiahao.me/tags/%E6%B2%B3%E5%8D%97/"/>
    <category term="山西" scheme="https://luanjiahao.me/tags/%E5%B1%B1%E8%A5%BF/"/>
    <content>
      <![CDATA[<h2 id="The-hesitation-before-setting-out"><a href="#The-hesitation-before-setting-out" class="headerlink" title="The hesitation before setting out"></a>The hesitation before setting out</h2><p>To be honest, I almost gave up the night before departure.</p><p>The weather forecast said it would rain over the weekend, and I knew all too well how slippery the scree slopes of the South Taihang can get in the rain. But the plane tickets were already bought, the chartered car was booked, and a few guys from the group chat had already arrived in Xinxiang. Whatever, to hell with it.</p><p>We left Zhengzhou at four in the morning, before dawn. The driver, Lao Zhang, was a local and drove three hours along mountain roads before dropping us at the entrance of Mawuzhai Village. A dog at the village entrance glanced at us and went back to sleep, probably used to people like us carrying big backpacks.</p><h2 id="The-scree-slope-taught-me-a-lesson"><a href="#The-scree-slope-taught-me-a-lesson" class="headerlink" title="The scree slope taught me a lesson"></a>The scree slope taught me a lesson</h2><p>The first two kilometers were fairly friendly, following a stream valley, with water so clear you could see the stones underneath. I even joked with my teammates that this trip would be easy.</p><p>Then came the scree slope.</p><p>The scree slopes of the South Taihang—how should I put it—are the kind of terrain where you step on one stone and three more shift beneath it. With a 20 kg pack on my back, I took every step cautiously. Halfway up, a guy ahead of me slipped, landed on his butt, and slid down two meters, tearing his backpack rain cover to shreds.</p><p>Watching from behind, I quietly tightened my grip on my trekking poles.</p><h2 id="Qixingtan"><a href="#Qixingtan" class="headerlink" title="Qixingtan"></a>Qixingtan</h2><p>After crossing the scree slope, my knees were already shaking. But when we rounded a bend, Qixingtan suddenly appeared before us.</p><p>In that moment, you really do freeze.</p><p>Emerald-green water, red rocks, and cliffs like knife cuts on both sides. Sunlight shone down through the gaps, and the water surface glittered. I stood there watching for several minutes, forgetting that my knees hurt.</p><p>One teammate said it looked like Jiuzhaigou. I thought it was wilder than Jiuzhaigou. Jiuzhaigou is too refined; this place still feels a bit desolate, a bit dangerous, but that is exactly the feeling that gets you addicted.</p><h2 id="Hongshi-River"><a href="#Hongshi-River" class="headerlink" title="Hongshi River"></a>Hongshi River</h2><p>We continued forward and reached the Hongshi River.</p><p>The entire riverbed was made of red stones, the water not fast, clear and cool. A few of us took off our shoes, sat on the rocks soaking our feet, and ate compressed biscuits. Nearby, an older woman was washing clothes. She glanced at us, probably thinking this bunch of city people were crazy.</p><p>The afternoon sun was lovely, making people lazy. I lay on the rocks, watching the clouds in the sky, and suddenly felt that this was how life should be. Not facing a computer in the office, not being squeezed into a photo in the subway, but lying on the rocks of the Taihang Mountains, listening to the water and spacing out.</p><h2 id="Night-in-Baodu-Village"><a href="#Night-in-Baodu-Village" class="headerlink" title="Night in Baodu Village"></a>Night in Baodu Village</h2><p>At night we stayed in Baodu Village.</p><p>The village sits above the cliffs, with only one road in and out—truly isolated from the world. There were only a few households. The owner of the farmhouse where we stayed made especially delicious noodles, and I ate three bowls.</p><p>At night there was no signal and no wifi. A few of us sat in the courtyard watching the stars. The starry sky over the Taihang Mountains—how should I put it—was the kind that makes you look up and feel it is a little unreal. The Milky Way hung clearly in the sky, and occasionally a shooting star streaked past.</p><p>One teammate said the last time he had seen stars was as a child back home. I said the same.</p><p>In that moment, I suddenly felt that we work so hard and make so much money, and in the end maybe all we want is to get back that feeling of lying in the courtyard as a child watching the stars.</p><h2 id="The-collapse-on-the-second-day"><a href="#The-collapse-on-the-second-day" class="headerlink" title="The collapse on the second day"></a>The collapse on the second day</h2><p>When I got up the second morning, my legs no longer felt like my own.</p><p>From Baodu Village to Hankou, they say it is mostly downhill, but the downhills in the South Taihang are also the kind that kill you. Stone-slab paths, cliffside plank walkways, dense forests to weave through. On one stretch we walked right against the cliff. I glanced down and my legs went weak.</p><p>I am a bit afraid of heights.</p><p>But we had already come this far; I couldn’t exactly fly down. I stuck close to the mountain wall and shuffled along step by step, my palms covered in sweat. A teammate ahead shouted, “Don’t look down!” I thought, if you hadn’t said that, I wouldn’t have wanted to look anyway.</p><h2 id="Baligou"><a href="#Baligou" class="headerlink" title="Baligou"></a>Baligou</h2><p>Near Hankou, we passed through Baligou.</p><p>Some people call it the Jiuzhaigou of the north. The water really is clear—so clear you can see the fish at the bottom. But I think what makes Baligou better than Jiuzhaigou is that there are not as many people. When we went, there were only a few of us in the entire scenic area.</p><p>Sitting beneath the waterfall, with the mist hitting my face, it felt cool. I suddenly remembered the days last year when I worked overtime in the office until 3 a.m., and felt that this moment was like a dream.</p><h2 id="The-return-trip"><a href="#The-return-trip" class="headerlink" title="The return trip"></a>The return trip</h2><p>We came out at Hankou, took the chartered car back to Xinxiang, and then the high-speed rail back to Zhengzhou.</p><p>On the train, I leaned against the window, watching the scenery recede rapidly outside. My legs hurt like hell, I had two blisters on my feet, and my shoulders had two red marks from the backpack straps. But inside I was satisfied.</p><p>After my phone regained signal, the group chat had more than a hundred new messages. My boss asked whether I could arrive early on Monday, and my colleagues were discussing next quarter’s KPIs. I turned off my phone and looked out the window for a while longer.</p><h2 id="Final-notes"><a href="#Final-notes" class="headerlink" title="Final notes"></a>Final notes</h2><p>The South Taihang route is honestly not easy. A 20 kg load, scree slopes, cliffside plank walkways, fear of heights. After finishing, I swore I wouldn’t carry a backpack again for three months.</p><p>But two months later, I was already reading guides for Wugong Mountain again.</p><p>People really are gluttons for punishment.</p><p>Some say hiking is self-torture. I don’t think so. Hiking lets you temporarily leave the world kidnapped by your phone, your work, and all kinds of deadlines, and return to the simplest state of life—walking, eating, sleeping, looking at scenery.</p><p>That simple.</p><blockquote><p><em>“The Taihang Mountains taught me one thing: the path is made by walking, not by thinking.”</em></p></blockquote><h2 id="Related-album-and-footprints"><a href="#Related-album-and-footprints" class="headerlink" title="Related album and footprints"></a>Related album and footprints</h2><p>More photos are collected in the <a href="/gallery/nantaihang/">South Taihang album</a>, and you can also continue looking for other destinations from the <a href="/map/">footprints map</a>.</p>]]>
    </content>
    <id>https://luanjiahao.me/2026/03/20/nantaihang-hiking/</id>
    <link href="https://luanjiahao.me/2026/03/20/nantaihang-hiking/"/>
    <published>2026-03-20T10:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="The-hesitation-before-setting-out"><a href="#The-hesitation-before-setting-out" class="headerlink" title="The hesitation before setting out"></a>The hesitation before setting out</h2><p>To be honest, I almost gave up the night before departure.</p>
<p>The weather forecast said it would rain]]>
    </summary>
    <title>South Taihang Heavy-Pack Traverse: From Mawuzhai to Hankou</title>
    <updated>2026-10-28T04:22:26.317Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="随笔" scheme="https://luanjiahao.me/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="徒步" scheme="https://luanjiahao.me/tags/%E5%BE%92%E6%AD%A5/"/>
    <category term="思考" scheme="https://luanjiahao.me/tags/%E6%80%9D%E8%80%83/"/>
    <category term="生活" scheme="https://luanjiahao.me/tags/%E7%94%9F%E6%B4%BB/"/>
    <content>
      <![CDATA[<h2 id="The-Question-I’ve-Been-Asked"><a href="#The-Question-I’ve-Been-Asked" class="headerlink" title="The Question I’ve Been Asked"></a>The Question I’ve Been Asked</h2><p>“Why do you go hiking?”</p><p>I’ve been asked this question many times. Colleagues ask, friends ask, family asks. They watch me leave the house on weekends with a big backpack and come back on Monday limping, and they find it very hard to understand.</p><p>“Wouldn’t it be better to just lie at home with that time?”</p><p>“Spending money to buy suffering?”</p><p>“Is something weighing on your mind?”</p><p>To be honest, I’ve asked myself this question too.</p><h2 id="The-Original-Reason"><a href="#The-Original-Reason" class="headerlink" title="The Original Reason"></a>The Original Reason</h2><p>At first, I went hiking because I wanted to lose weight.</p><p>At that time, I had just been working for two years. I worked overtime every day, ordered takeout, sat for long periods, and my weight shot up to 170 jin. Several arrows were pointing up on my health check report, and the doctor said I couldn’t keep going like this.</p><p>I wanted to exercise, but running was too boring, the gym was too expensive, and playing ball required rounding up people. Later I saw people hiking and thought this was good: it didn’t cost money, didn’t require rounding up people, and I could do it alone.</p><p>So I went.</p><h2 id="The-First-Hike"><a href="#The-First-Hike" class="headerlink" title="The First Hike"></a>The First Hike</h2><p>My first hike was to a small local hill, less than ten kilometers.</p><p>I thought it would be very simple, so I went in sneakers. As a result, halfway through, my feet started hurting. The soles were too soft, and stepping on rocks was painfully bumpy. When I reached the summit, I was dog-tired and sat on the ground panting for half an hour.</p><p>But on the way down, I suddenly felt that this kind of tiredness was different from the tiredness of working overtime.</p><p>The tiredness of overtime is the kind that is pent up inside. The body isn’t tired, but the mind is. The tiredness of hiking is a pure kind of tiredness: legs sore, sweating, panting, but the mind is blank.</p><p>At that moment I seemed to understand why some people like hiking.</p><h2 id="Later"><a href="#Later" class="headerlink" title="Later"></a>Later</h2><p>Later I started walking farther.</p><p>From ten kilometers to twenty kilometers, from day hikes to multi-day backpacking traverses. My gear also became more and more specialized: from sneakers to hiking boots, from a daypack to a heavy-load backpack, from bottled water to a thermos.</p><p>My weight didn’t drop much, probably because I ate more after hiking.</p><p>But I found that what hiking gave me was much more important than losing weight.</p><h2 id="What-Hiking-Taught-Me"><a href="#What-Hiking-Taught-Me" class="headerlink" title="What Hiking Taught Me"></a>What Hiking Taught Me</h2><h3 id="First-Focus"><a href="#First-Focus" class="headerlink" title="First: Focus"></a>First: Focus</h3><p>When hiking, you can only think about one thing: walking.</p><p>Watch your feet, where to step, how to take the next step. Your mind is filled with these simple things, with no space to think about work troubles, no space to think about the mortgage and KPIs.</p><p>This feeling of focus is too scarce in daily life. Every day we are interrupted by phones, by messages, by all kinds of things, and it is hard to truly focus on one thing. But when hiking, you have to focus.</p><h3 id="Second-Patience"><a href="#Second-Patience" class="headerlink" title="Second: Patience"></a>Second: Patience</h3><p>Hiking is a slow process.</p><p>You can’t get to the summit all at once; you have to walk step by step. Sometimes after walking for two hours, you look up and the summit is still in the clouds. That feeling can make you break down, but you have no choice but to keep walking.</p><p>This is very much like life. Many goals can’t be achieved all at once; you have to take your time. But modern people are too impatient. They want everything fast: quick success, getting rich quickly, losing weight quickly.</p><p>Hiking taught me that some things can’t be rushed.</p><h3 id="Third-Acceptance"><a href="#Third-Acceptance" class="headerlink" title="Third: Acceptance"></a>Third: Acceptance</h3><p>When hiking, you will encounter many unexpected things.</p><p>Weather changes, taking the wrong route, gear problems, physical discomfort. You can’t control these things. You can only accept them and then figure out how to respond.</p><p>Life is like this too. Plans can’t keep up with changes. All you can do is accept reality and then move forward.</p><h3 id="Fourth-Simplicity"><a href="#Fourth-Simplicity" class="headerlink" title="Fourth: Simplicity"></a>Fourth: Simplicity</h3><p>When hiking, life becomes very simple.</p><p>Walking, eating, sleeping. No complicated interpersonal relationships, no annoying work emails, no meaningless socializing. You only need to pay attention to the most basic needs: water, food, rest.</p><p>This simple feeling is addictive.</p><h2 id="On-Meaning"><a href="#On-Meaning" class="headerlink" title="On Meaning"></a>On Meaning</h2><p>Someone asked me, what is the meaning of hiking?</p><p>To be honest, I don’t know.</p><p>Hiking can’t get me promoted or get me a raise, can’t buy me a house or a car, can’t make me more handsome or more charming. On the contrary, it costs me money, gets me injured, and makes me tan.</p><p>But I just like it.</p><p>I like the feeling of walking in the mountains, the feeling of standing at the summit looking at the scenery, the feeling of lying in a tent at night listening to the rain. These feelings can’t be eaten as food, but they make food taste better.</p><h2 id="On-Solitude"><a href="#On-Solitude" class="headerlink" title="On Solitude"></a>On Solitude</h2><p>Hiking is mostly solitary.</p><p>Walking alone in the mountains, with no one to chat with, you can only talk to yourself. Sometimes you think about many things, sometimes you think about nothing.</p><p>I used to fear loneliness and thought that being alone would make my mind run wild. But after hiking I found that solitude isn’t so scary. On the contrary, when you are alone, you can truly face yourself.</p><p>Those problems that are usually covered up by busyness emerge in solitude. You have to face them, solve them, or accept them.</p><p>This is probably the biggest gift hiking has given me.</p><h2 id="In-Closing"><a href="#In-Closing" class="headerlink" title="In Closing"></a>In Closing</h2><p>Why go hiking?</p><p>I can answer this question now.</p><p>It’s not to lose weight, not to post on WeChat Moments, not to prove anything. It’s just to temporarily leave that world held hostage by phones, work, and all kinds of deadlines, and return to the simplest state of life.</p><p>Walking, eating, sleeping, looking at the scenery.</p><p>As simple as that.</p><p>Some people say hiking is escape. I don’t think so. Escape is not wanting to face things; hiking is for facing them better. After you’ve gone out and come back, those problems are still there, but the angle from which you look at them has changed.</p><p>This is the meaning of hiking.</p><p>Not escape, but recharging.</p><blockquote><p><em>“The mountain is there, so I go.”</em></p></blockquote>]]>
    </content>
    <id>https://luanjiahao.me/2026/03/01/why-i-hike/</id>
    <link href="https://luanjiahao.me/2026/03/01/why-i-hike/"/>
    <published>2026-03-01T12:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="The-Question-I’ve-Been-Asked"><a href="#The-Question-I’ve-Been-Asked" class="headerlink" title="The Question I’ve Been Asked"></a>The Question I’ve Been Asked</h2><p>“Why do you go hiking?”</p>
<p>I’ve been asked this question many times. Colleagues ask, friends ask, family asks. They watch]]>
    </summary>
    <title>Why Go Hiking?</title>
    <updated>2026-10-28T04:22:26.313Z</updated>
  </entry>
  <entry>
    <author>
      <name>YFGUG</name>
    </author>
    <category term="徒步游记" scheme="https://luanjiahao.me/categories/%E5%BE%92%E6%AD%A5%E6%B8%B8%E8%AE%B0/"/>
    <category term="失败" scheme="https://luanjiahao.me/tags/%E5%A4%B1%E8%B4%A5/"/>
    <category term="徒步" scheme="https://luanjiahao.me/tags/%E5%BE%92%E6%AD%A5/"/>
    <category term="教训" scheme="https://luanjiahao.me/tags/%E6%95%99%E8%AE%AD/"/>
    <content>
      <![CDATA[<h2 id="Admitting-Failure"><a href="#Admitting-Failure" class="headerlink" title="Admitting Failure"></a>Admitting Failure</h2><p>I’m writing this today to record a failed hike.</p><p>Not one of those stories where “it was exhausting, but I made it in the end”—I really didn’t finish. I turned back halfway. I used to think that kind of thing was embarrassing and didn’t want to talk about it. But later I realized failure is part of the experience too. It’s not shameful.</p><h2 id="Setting-Out"><a href="#Setting-Out" class="headerlink" title="Setting Out"></a>Setting Out</h2><p>This happened last December.</p><p>I wanted to hike a local point-to-point route. It wasn’t long—about 15 kilometers, doable in one day. I checked the weather forecast: sunny, around 5 degrees. I figured there was no problem, so I packed my bag and set off.</p><p>My condition that day wasn’t actually great. I’d had insomnia the night before and only slept four hours. I had a bit of a headache when I got up, but I thought walking would make it better.</p><p>That was my first mistake: pushing through even though I didn’t feel well.</p><h2 id="Changes-in-the-Mountains"><a href="#Changes-in-the-Mountains" class="headerlink" title="Changes in the Mountains"></a>Changes in the Mountains</h2><p>When I entered the mountains, the weather was still decent. The sunlight felt warm on my body.</p><p>But after walking 2 kilometers, the sky began to cloud over. I knew mountain weather changes quickly, but I didn’t expect it to change that quickly. At 10 o’clock the wind started picking up; by 11 it began to sleet.</p><p>I was wearing a hardshell jacket and fleece pants. They were waterproof, yes, but not warm. Once the wind blew, I was shivering with cold. I picked up my pace, hoping to hurry to the pavilion at the summit and take shelter for a while.</p><p>That was my second mistake: I didn’t adjust my plan in time when the weather changed.</p><h2 id="Slipping"><a href="#Slipping" class="headerlink" title="Slipping"></a>Slipping</h2><p>Halfway there, the trail started to get slippery.</p><p>Sleet fell on the rocks and formed a thin layer of ice. I was wearing ordinary hiking shoes, and the tread on the soles had already worn smooth. I took every step carefully, but I still slipped and fell.</p><p>It was the kind of fall where your feet slide out and you land on your backside. It wasn’t serious—just wet pants and a sore backside. I got up and kept walking, but I started to feel uneasy.</p><p>After another few hundred meters, I slipped again. This time I slid sideways, knocked my knee against a rock, and gasped in pain.</p><p>I crouched there, looked at the bruise on my knee, and seriously began to consider whether I should turn back.</p><h2 id="The-Decision-to-Retreat"><a href="#The-Decision-to-Retreat" class="headerlink" title="The Decision to Retreat"></a>The Decision to Retreat</h2><p>To be honest, the decision to retreat wasn’t easy to make.</p><p>I had already walked 6 kilometers. Another 3 kilometers would take me to the summit, and then it would be downhill. If I turned back now, the day would be wasted. Besides, I had told my friends I was going to hike this route—going back empty-handed would be so embarrassing.</p><p>But then I thought about it again: if I kept going, there were still 6 kilometers of trail left, the weather was getting worse, my knee was already bruised, my shoes had no grip, and my phone signal kept coming and going. If something happened, even calling for rescue would be inconvenient.</p><p>In the end, I decided to retreat.</p><p>Face isn’t worth much; knees are.</p><h2 id="The-Way-Down"><a href="#The-Way-Down" class="headerlink" title="The Way Down"></a>The Way Down</h2><p>The way down was even harder than the way up.</p><p>Going uphill, I could still control my center of gravity; going downhill, my legs were shaking and I couldn’t control them at all. I practically squatted and shuffled down step by step, planting my trekking poles on the ground until my hands went numb from gripping them.</p><p>By the time I reached the foot of the mountain, it was already dark. I got into the car, turned on the heater, changed into dry clothes, and then realized my hands were shaking.</p><p>Not from the cold—from lingering fear.</p><h2 id="Lessons"><a href="#Lessons" class="headerlink" title="Lessons"></a>Lessons</h2><p>After that, I set a few rules for myself:</p><ol><li><p><strong>Don’t push through when you feel unwell.</strong> Insomnia, a cold, a headache—any kind of discomfort means don’t go into the mountains.</p></li><li><p><strong>Adjust promptly when the weather changes.</strong> If the wind picks up, add clothes; if it starts to rain, consider retreating. Don’t gamble on luck.</p></li><li><p><strong>Your shoes must have good grip.</strong> I later switched to shoes with Vibram outsoles and never slipped again.</p></li><li><p><strong>Your phone needs signal.</strong> Check the route before you go—where there’s signal and where there isn’t. If there’s no signal along the entire route, either don’t go or bring a satellite phone.</p></li><li><p><strong>Retreating isn’t shameful.</strong> The mountain is always there; you can come back next time. But if your knee is ruined, that’s really the end.</p></li></ol><h2 id="Later"><a href="#Later" class="headerlink" title="Later"></a>Later</h2><p>Later I hiked that route again.</p><p>I went in spring. The weather was great, and flowers bloomed all over the mountains. I completed the whole route, stood at the summit, looked at the distant scenery, and thought about that retreat in winter.</p><p>I didn’t feel regret; instead, I felt a little lucky. If I hadn’t retreated that time, I might not have dared to come back.</p><p>Failure isn’t scary; what’s scary is not daring to try again because of one failure.</p><blockquote><p><em>“The mountain will always be there, but you only have one body.”</em></p></blockquote>]]>
    </content>
    <id>https://luanjiahao.me/2026/02/15/failed-hiking-trip/</id>
    <link href="https://luanjiahao.me/2026/02/15/failed-hiking-trip/"/>
    <published>2026-02-15T11:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="Admitting-Failure"><a href="#Admitting-Failure" class="headerlink" title="Admitting Failure"></a>Admitting Failure</h2><p>I’m writing this today to record a failed hike.</p>
<p>Not one of those stories where “it was exhausting, but I made it in the end”—I really didn’t finish. I turned back]]>
    </summary>
    <title>The Hike I Didn't Finish</title>
    <updated>2026-10-28T04:22:26.321Z</updated>
  </entry>
</feed>
