<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>李理的博客</title>
    <description></description>
    <link>http://fancyerii.github.io/</link>
    <atom:link href="http://fancyerii.github.io/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Tue, 30 Dec 2025 03:06:24 +0000</pubDate>
    <lastBuildDate>Tue, 30 Dec 2025 03:06:24 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>《C++ High Performance: Master the art of optimizing the functioning of your C++ code , Second Edition》学习</title>
        <description>&lt;p&gt;&lt;!--more--&gt;&lt;/p&gt;

&lt;h3 id=&quot;第一章c-简介&quot;&gt;&lt;a href=&quot;/chp/ch1&quot;&gt;第一章：C++ 简介&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第二章c-基础技巧&quot;&gt;&lt;a href=&quot;/chp/ch2&quot;&gt;第二章：C++ 基础技巧&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第三章分析和测量性能&quot;&gt;&lt;a href=&quot;/chp/ch3&quot;&gt;第三章：分析和测量性能&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第四章数据结构&quot;&gt;&lt;a href=&quot;/chp/ch4&quot;&gt;第四章：数据结构&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第五章算法&quot;&gt;&lt;a href=&quot;/chp/ch5&quot;&gt;第五章：算法&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第六章范围与视图&quot;&gt;&lt;a href=&quot;/chp/ch6&quot;&gt;第六章：范围与视图&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第七章内存管理&quot;&gt;&lt;a href=&quot;/chp/ch7&quot;&gt;第七章：内存管理&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第八章编译时编程&quot;&gt;&lt;a href=&quot;/chp/ch8&quot;&gt;第八章：编译时编程&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第九章实用工具&quot;&gt;&lt;a href=&quot;/chp/ch9&quot;&gt;第九章：实用工具&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第十章代理对象与惰性求值&quot;&gt;&lt;a href=&quot;/chp/ch10&quot;&gt;第十章：代理对象与惰性求值&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第十一章并发&quot;&gt;&lt;a href=&quot;/chp/ch11&quot;&gt;第十一章：并发&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第十二章协程和惰性生成器&quot;&gt;&lt;a href=&quot;/chp/ch12&quot;&gt;第十二章：协程和惰性生成器&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第十三章使用协程进行异步编程&quot;&gt;&lt;a href=&quot;/chp/ch13&quot;&gt;第十三章：使用协程进行异步编程&lt;/a&gt;&lt;/h3&gt;
&lt;h3 id=&quot;第十四章并行算法&quot;&gt;&lt;a href=&quot;/chp/ch14&quot;&gt;第十四章：并行算法&lt;/a&gt;&lt;/h3&gt;

</description>
        <pubDate>Tue, 30 Dec 2025 00:00:00 +0000</pubDate>
        <link>http://fancyerii.github.io/2025/12/30/chp/</link>
        <guid isPermaLink="true">http://fancyerii.github.io/2025/12/30/chp/</guid>
        
        <category>c++</category>
        
        
      </item>
    
      <item>
        <title>Implementing and Optimizing a BPE Tokenizer from Scratch—Part 11: Wrapping C++ Code with Cython</title>
        <description>&lt;p&gt;This series of articles implements a subtask of Stanford’s CS336 Assignment 1: building an efficient training algorithm for a BPE Tokenizer. Through a series of optimizations, our algorithm’s training time on OpenWebText was reduced from over 10 hours to less than 10 minutes. This series explains these optimizations, including algorithmic improvements, data structure enhancements, parallelization with OpenMP, Cython optimization, and implementing key code in C++ along with its integration via Cython. This is the twelfth and final article in a series on using Cython to wrap the previous C++ code into an extension module for Python.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;strong&gt;Table of Content&lt;/strong&gt;&lt;/p&gt;
&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#1-goal&quot; id=&quot;markdown-toc-1-goal&quot;&gt;1. Goal&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-wrapping-the-c-code-into-a-dynamic-library&quot; id=&quot;markdown-toc-2-wrapping-the-c-code-into-a-dynamic-library&quot;&gt;2. Wrapping the C++ Code into a Dynamic Library&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-header-file&quot; id=&quot;markdown-toc-21-header-file&quot;&gt;2.1 Header File&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-cmakeliststxt&quot; id=&quot;markdown-toc-22-cmakeliststxt&quot;&gt;2.2 CMakeLists.txt&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-bpe_train_step2&quot; id=&quot;markdown-toc-23-bpe_train_step2&quot;&gt;2.3 bpe_train_step2&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#24-bpe_train_step2_v2&quot; id=&quot;markdown-toc-24-bpe_train_step2_v2&quot;&gt;2.4 bpe_train_step2_v2&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#25-compile-and-install&quot; id=&quot;markdown-toc-25-compile-and-install&quot;&gt;2.5 Compile and Install&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-wrapping-with-cython-into-an-extension-module&quot; id=&quot;markdown-toc-3-wrapping-with-cython-into-an-extension-module&quot;&gt;3. Wrapping with Cython into an Extension Module&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-bpe_train_step2_wrapperpxd&quot; id=&quot;markdown-toc-31-bpe_train_step2_wrapperpxd&quot;&gt;3.1 bpe_train_step2_wrapper.pxd&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-bpe_train_step2_wrapperpyx&quot; id=&quot;markdown-toc-32-bpe_train_step2_wrapperpyx&quot;&gt;3.2 bpe_train_step2_wrapper.pyx&lt;/a&gt;        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#321-py_bpe_train_step2&quot; id=&quot;markdown-toc-321-py_bpe_train_step2&quot;&gt;3.2.1 py_bpe_train_step2&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#322-py_bpe_train_step2_v2&quot; id=&quot;markdown-toc-322-py_bpe_train_step2_v2&quot;&gt;3.2.2 py_bpe_train_step2_v2&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-modifying-setuppy&quot; id=&quot;markdown-toc-33-modifying-setuppy&quot;&gt;3.3 Modifying setup.py&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#34-using-it-in-python&quot; id=&quot;markdown-toc-34-using-it-in-python&quot;&gt;3.4 Using it in Python&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-testing&quot; id=&quot;markdown-toc-4-testing&quot;&gt;4. Testing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-conclusion&quot; id=&quot;markdown-toc-5-conclusion&quot;&gt;5. Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the English translation, with Markdown formatting and code blocks preserved:&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;1-goal&quot;&gt;1. Goal&lt;/h2&gt;

&lt;p&gt;In previous articles, we’ve done a lot of exploration. BPE training can be divided into two stages. The first stage is tokenization and counting, which we can perform in parallel using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;multiprocessing&lt;/code&gt;. Our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v3.py&lt;/code&gt; implementation can complete this in 120 seconds using 32 cores, while the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v3_bytes_time.py&lt;/code&gt; version, which reads bytes, can finish in 70 seconds using 64 cores. The second stage is merging. The Python version, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v8_v3.py&lt;/code&gt;, takes about 500 seconds, with a total time of over ten minutes. The fastest C++ version, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp&lt;/code&gt;, has a merging time of around 100 seconds.&lt;/p&gt;

&lt;p&gt;Today, our goal is to create the fastest version possible by wrapping the C++ code into an extension module using Cython, which can then be called by Python for the second stage.&lt;/p&gt;

&lt;h2 id=&quot;2-wrapping-the-c-code-into-a-dynamic-library&quot;&gt;2. Wrapping the C++ Code into a Dynamic Library&lt;/h2&gt;

&lt;p&gt;We’ve conducted many experiments in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cppupdate&lt;/code&gt;, and now we need to wrap the best-performing versions into a dynamic library for easy use with Cython.&lt;/p&gt;

&lt;p&gt;We will create a new C++ project called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cppstep2&lt;/code&gt;. The full code can be found at &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/tree/main/cppstep2&quot;&gt;cppstep2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Based on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cppupdate&lt;/code&gt; experiment results, I have selected the following files: &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8.cpp&lt;/a&gt;, &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8_set.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8_set.cpp&lt;/a&gt;, &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8_set9.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8_set9.cpp&lt;/a&gt;, &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_emhash8_set9.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9.cpp&lt;/a&gt;, and &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I chose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash&lt;/code&gt; over &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl&lt;/code&gt; for two reasons: it’s faster and simpler to use. I previously &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone&lt;/code&gt;d the full &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash&lt;/code&gt; code, but to simplify, I’ve only copied the three necessary header files: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash_set8.hpp&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash_set4.hpp&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash_table8.hpp&lt;/code&gt;. Additionally, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_heap.h&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_heap.cpp&lt;/code&gt; were copied directly from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cppupdate&lt;/code&gt; project.&lt;/p&gt;

&lt;p&gt;Our dynamic library will consist of two main files: the header file &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppstep2/bpe_train_step2.h&quot;&gt;bpe_train_step2.h&lt;/a&gt; and the implementation file &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppstep2/bpe_train_step2.cpp&quot;&gt;bpe_train_step2.cpp&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;21-header-file&quot;&gt;2.1 Header File&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#pragma once

#include &amp;lt;vector&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
#include &amp;lt;unordered_set&amp;gt;
#include &quot;emhash/hash_table8.hpp&quot;

struct pair_hash {
    template &amp;lt;class T1, class T2&amp;gt;
    std::size_t operator () (const std::pair&amp;lt;T1,T2&amp;gt; &amp;amp;p) const {
        auto h1 = std::hash&amp;lt;T1&amp;gt;{}(p.first);
        auto h2 = std::hash&amp;lt;T2&amp;gt;{}(p.second);

        return h1 ^ (h2 + 0x9e3779b9 + (h1 &amp;lt;&amp;lt; 6) + (h1 &amp;gt;&amp;gt; 2));
    }
};

void bpe_train_step2(int vocab_size, 
                emhash8::HashMap&amp;lt;std::pair&amp;lt;int, int&amp;gt;, int, pair_hash&amp;gt; &amp;amp; pair_counts, 
                std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;, pair_hash&amp;gt; &amp;amp; pair_strings, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::unordered_set&amp;lt;int&amp;gt;, pair_hash&amp;gt; &amp;amp; pair_wordids, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges);

void bpe_train_step2_v2(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges);

void bpe_train_step2_v3(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges);   
                
void bpe_train_step2_v4(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges);  

void bpe_train_step2_v5(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges);  

void bpe_train_step2_v6(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges); 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This header file defines five functions: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_v2&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_v3&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_v4&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_v5&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_v6&lt;/code&gt;. These correspond to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8.cpp&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8_set.cpp&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8_set9.cpp&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9.cpp&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp&lt;/code&gt;, respectively.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2&lt;/code&gt; is a direct copy of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cppupdate&lt;/code&gt; implementation and requires seven input parameters. As we’ll see later, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wordid_counts&lt;/code&gt; can be calculated inside C++ for better speed and fewer parameters passed. Thus, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_v2&lt;/code&gt; and subsequent versions only require five parameters.&lt;/p&gt;

&lt;h3 id=&quot;22-cmakeliststxt&quot;&gt;2.2 CMakeLists.txt&lt;/h3&gt;

&lt;p&gt;Our goal is to compile a dynamic library. We’ll write the following in CMake syntax:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cmake_minimum_required(VERSION 3.20)

project(BPE_TRAIN_STEP2 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)


add_library(bpe_train_step2 SHARED bpe_train_step2.cpp max_heap.cpp)
target_include_directories(bpe_train_step2 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(bpe_train_step2 PUBLIC MY_LIBRARY_EXPORT)

target_include_directories(bpe_train_step2 PUBLIC
    &quot;${PROJECT_SOURCE_DIR}/emhash&quot;
)

install(TARGETS bpe_train_step2
        EXPORT bpe_train_step2_export_targets
        RUNTIME DESTINATION bin
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib)

install(FILES &quot;bpe_train_step2.h&quot; 
        DESTINATION include
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We create the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2&lt;/code&gt; target using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;add_library&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target_compile_definitions&lt;/code&gt; seems to define a macro for Windows compatibility. I don’t have a Windows environment to test this, so I’m unsure if the project will compile there.&lt;/p&gt;

&lt;p&gt;The next &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target_include_directories&lt;/code&gt; includes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash&lt;/code&gt; headers. Finally, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;install&lt;/code&gt; copies the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2&lt;/code&gt; target (mainly &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libbpe_train_step2.so&lt;/code&gt;) and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2.h&lt;/code&gt; to the appropriate locations. I’ll explain the installation process below.&lt;/p&gt;

&lt;h3 id=&quot;23-bpe_train_step2&quot;&gt;2.3 bpe_train_step2&lt;/h3&gt;

&lt;p&gt;First, we will integrate it in the same way as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cppupdate&lt;/code&gt;, which means we need to pass seven parameters, including &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;void bpe_train_step2(int vocab_size, 
                emhash8::HashMap&amp;lt;std::pair&amp;lt;int, int&amp;gt;, int, pair_hash&amp;gt; &amp;amp; pair_counts, 
                std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;, pair_hash&amp;gt; &amp;amp; pair_strings, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::unordered_set&amp;lt;int&amp;gt;, pair_hash&amp;gt; &amp;amp; pair_wordids, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges){
    auto start = std::chrono::steady_clock::now();
    while(vocabulary.size() &amp;lt; vocab_size){
        int max_count = -1;
        std::pair&amp;lt;int, int&amp;gt; max_pair;
        std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt; max_strings;
        for(const auto&amp;amp; [pair, count] : pair_counts){
            if(count &amp;gt; max_count){
                max_count = count;
                max_pair = pair;
                max_strings = pair_strings[pair];
            }else if(count == max_count){
                std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt; strings = pair_strings[pair];
                ComparisonResult r1 = three_way_compare(strings[0], max_strings[0]);
                if(r1 == ComparisonResult::Greater){
                    max_count = count;
                    max_pair = pair;
                    max_strings = strings;
                }else if(r1 == ComparisonResult::Equal){
                    ComparisonResult r2 = three_way_compare(strings[1], max_strings[1]);
                    if(r2 == ComparisonResult::Greater){
                        max_count = count;
                        max_pair = pair;
                        max_strings = strings;                        
                    }
                }
            }
        }

        const std::vector&amp;lt;int&amp;gt;&amp;amp; bytes1 = vocabulary[max_pair.first];
        const std::vector&amp;lt;int&amp;gt;&amp;amp; bytes2 = vocabulary[max_pair.second];
        std::vector&amp;lt;int&amp;gt; merge_bytes;
        merge_bytes.reserve(bytes1.size() + bytes2.size());
        merge_bytes.insert(merge_bytes.end(), bytes1.begin(), bytes1.end());
        merge_bytes.insert(merge_bytes.end(), bytes2.begin(), bytes2.end());

        int size = vocabulary.size();
        vocabulary[size] = merge_bytes;

        auto&amp;amp; affected_words = pair_wordids[max_pair];

        
        updated_affected_word_count(max_pair, affected_words, wordid_encodings, wordid_counts,
                                    pair_counts, pair_wordids, size, pair_strings, vocabulary);
  
        merges.push_back({bytes1, bytes2});


    }
    auto end = std::chrono::steady_clock::now();
    auto duration = std::chrono::duration_cast&amp;lt;std::chrono::milliseconds&amp;gt;(end - start);
    std::cout &amp;lt;&amp;lt; &quot;bpe_train_step2: &quot; &amp;lt;&amp;lt; duration.count() &amp;lt;&amp;lt; &quot;ms.&quot; &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The code is a complete copy of &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8.cpp&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;24-bpe_train_step2_v2&quot;&gt;2.4 bpe_train_step2_v2&lt;/h3&gt;

&lt;p&gt;If you’ve carefully reviewed the previous time statistics, you’ll notice that the total time is about 50-60 seconds more than “tokenization time” + “merging time”. This extra time is for calling the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BPE_Trainer._count_pairs&lt;/code&gt; function:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    @staticmethod    
    def _count_pairs(word_counts, word_encodings, pair_strings, vocabulary, pair_to_words):
        pair_counts = defaultdict(int)
        for word, count in word_counts.items():
            encoding = word_encodings[word]
            for i in range(0, len(encoding) - 1):
                pair = encoding[i], encoding[i + 1]
                pair_counts[pair] += count
                if pair not in pair_strings:
                    pair_strings[pair] = (vocabulary[pair[0]], vocabulary[pair[1]])

                pair_to_words[pair].add(word)

        return pair_counts
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This code calculates &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; based on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;word_counts&lt;/code&gt; and builds the initial inverted index &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_to_words&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_strings&lt;/code&gt;. Its input parameters are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;word_counts&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;word_encodings&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vocabulary&lt;/code&gt;. This code can be implemented entirely in C++, which would not only speed it up but also reduce the number of parameters passed between Python and C++.&lt;/p&gt;

&lt;p&gt;Therefore, the input parameters for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_v2&lt;/code&gt; can be reduced by three:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;void bpe_train_step2_v2(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges){
    std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;, pair_hash&amp;gt; pair_strings;
    emhash8::HashMap&amp;lt;std::pair&amp;lt;int, int&amp;gt;, int, pair_hash&amp;gt;  pair_counts;
    std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::unordered_set&amp;lt;int&amp;gt;, pair_hash&amp;gt;  pair_wordids;
    
    std::pair&amp;lt;int, int&amp;gt; pair;
    for(const auto&amp;amp; [wordid, count] : wordid_counts){
        const auto&amp;amp; word_tokens = wordid_encodings[wordid];
        for(int i = 0; i &amp;lt; word_tokens.size() - 1; ++i){
            pair.first = word_tokens[i];
            pair.second = word_tokens[i + 1];
            pair_counts[pair] += count;
            if (pair_strings.find(pair) == pair_strings.end()) {
                pair_strings[pair] = {vocabulary[pair.first], vocabulary[pair.second]};
            }
            pair_wordids[pair].insert(wordid);
        }
    }
    

    bpe_train_step2(vocab_size, pair_counts, pair_strings, vocabulary,
                    pair_wordids, wordid_counts, wordid_encodings, merges);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_v2&lt;/code&gt; function first calculates &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_strings&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_wordids&lt;/code&gt; from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wordid_counts&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wordid_encodings&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vocabulary&lt;/code&gt;, and then calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_v3&lt;/code&gt; and subsequent versions have the exact same interface as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_v2&lt;/code&gt;, and the code is copied from the corresponding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cppupdate&lt;/code&gt; versions, which I won’t detail here.&lt;/p&gt;

&lt;h3 id=&quot;25-compile-and-install&quot;&gt;2.5 Compile and Install&lt;/h3&gt;

&lt;p&gt;Use the following commands to compile and install:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cd cppstep2
mkdir build &amp;amp;&amp;amp; cd build
cmake -D CMAKE_INSTALL_PREFIX=../../lib_bpe_train_step2/  -D CMAKE_BUILD_TYPE=Release ..
cmake --build . -- -j8
# If you are using gcc, you can directly use make -j8
cmake --install .  
# Or make install
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Since we don’t want to install this library into system paths like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local&lt;/code&gt; (which requires root privileges and is not convenient for Cython integration due to different paths on different systems), I’m installing it to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;../../lib_bpe_train_step2/&lt;/code&gt;, which is a subdirectory of the project root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assignment1-basics-bpe&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ls assignment1-basics-bpe

cppstep2/
cppupdate/
cs336_basics/
data/
lib_bpe_train_step2/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After installation, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib_bpe_train_step2&lt;/code&gt; directory will look like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;lib_bpe_train_step2$ tree
.
├── include
│   ├── bpe_train_step2.h
│   └── emhash
│       └── hash_table8.hpp
└── lib
    └── libbpe_train_step2.so
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Besides &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2.h&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash_table8.hpp&lt;/code&gt; is also included because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2.h&lt;/code&gt; uses it. This is primarily because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2&lt;/code&gt;’s declaration needs it. If we only kept &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_v2&lt;/code&gt; and later versions, this header file would not need to be installed (though we’d still need all &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash&lt;/code&gt; headers for compilation).&lt;/p&gt;

&lt;h2 id=&quot;3-wrapping-with-cython-into-an-extension-module&quot;&gt;3. Wrapping with Cython into an Extension Module&lt;/h2&gt;

&lt;p&gt;For those new to wrapping C++ libraries with Cython, you can refer to the &lt;a href=&quot;https://cython.readthedocs.io/en/stable/src/userguide/wrapping_CPlusPlus.html&quot;&gt;Using C++ in Cython&lt;/a&gt; guide.&lt;/p&gt;

&lt;p&gt;We need two files: &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_train_step2_wrapper.pxd&quot;&gt;bpe_train_step2_wrapper.pxd&lt;/a&gt; and &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_train_step2_wrapper.pyx&quot;&gt;bpe_train_step2_wrapper.pyx&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;31-bpe_train_step2_wrapperpxd&quot;&gt;3.1 bpe_train_step2_wrapper.pxd&lt;/h3&gt;

&lt;p&gt;First, let’s look at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.pxd&lt;/code&gt; file:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# distutils: language = c++

# 导入 C++ 标准库类型
from libcpp.utility cimport pair
from libcpp.vector cimport vector
from libcpp.unordered_map cimport unordered_map
from libcpp.unordered_set cimport unordered_set


cdef extern from &quot;../lib_bpe_train_step2/include/bpe_train_step2.h&quot; :
    cppclass pair_hash:
        pass

cdef extern from &quot;../lib_bpe_train_step2/include/emhash/hash_table8.hpp&quot; namespace &quot;emhash8&quot;:
    cppclass HashMap[K, V, H]:
        #ValueT&amp;amp; operator[](const KeyT&amp;amp; key) noexcept
        V&amp;amp; operator[](const K&amp;amp; key)


cdef extern from &quot;../lib_bpe_train_step2/include/bpe_train_step2.h&quot;:
    void bpe_train_step2(int vocab_size,
                         HashMap[pair[int, int], int, pair_hash] &amp;amp; pair_counts,
                         unordered_map[pair[int, int], vector[vector[int]], pair_hash] &amp;amp; pair_strings,
                         unordered_map[int, vector[int]] &amp;amp; vocabulary,
                         unordered_map[pair[int, int], unordered_set[int], pair_hash] &amp;amp; pair_wordids,
                         const unordered_map[int, long long] &amp;amp; wordid_counts,
                         unordered_map[int, vector[int]] &amp;amp; wordid_encodings,
                         vector[pair[vector[int], vector[int]]] &amp;amp; merges) except +
    
    void bpe_train_step2_v2(int vocab_size,
                         unordered_map[int, vector[int]] &amp;amp; vocabulary,
                         const unordered_map[int, long long] &amp;amp; wordid_counts,
                         unordered_map[int, vector[int]] &amp;amp; wordid_encodings,
                         vector[pair[vector[int], vector[int]]] &amp;amp; merges) except +


&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The first line tells &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;distutils&lt;/code&gt; (or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setuptools&lt;/code&gt;) that you are building a C++ extension module, not the default C extension.&lt;/p&gt;

&lt;p&gt;The next four lines import the C++ standard library types &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::vector&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::pair&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_set&lt;/code&gt; which are wrapped by Cython under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libcpp&lt;/code&gt;. We can use them with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cimport&lt;/code&gt;, which is a compile-time import. To see which C++ standard libraries are available, refer to &lt;a href=&quot;https://github.com/cython/cython/tree/master/Cython/Includes/libcpp&quot;&gt;this link&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, we declare &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_hash&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cppclass&lt;/code&gt;. Since we only need to know about the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_hash&lt;/code&gt; symbol for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib_bpe_train_step2&lt;/code&gt; library, we can leave the content empty with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pass&lt;/code&gt;. Note that the path for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cdef extern from&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;../lib_bpe_train_step2/include/bpe_train_step2.h&quot;&lt;/code&gt;, which requires &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib_bpe_train_step2&lt;/code&gt; to be installed in the correct location as described earlier.&lt;/p&gt;

&lt;p&gt;Next is the declaration of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashMap&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;namespace emhash8&quot;&lt;/code&gt; after &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cdef extern from&lt;/code&gt; tells Cython that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HashMap&lt;/code&gt; is in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8&lt;/code&gt; namespace. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cppclass HashMap[K, V, H]&lt;/code&gt; indicates that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HashMap&lt;/code&gt; is a template class for Key/Value/Hash function. We’ll need &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;operator[]&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.pyx&lt;/code&gt; file, so we also need to declare its prototype here.&lt;/p&gt;

&lt;p&gt;Finally, we declare the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2&lt;/code&gt; functions. The syntax is similar to C++, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt; &amp;gt;&lt;/code&gt; is replaced with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[ ]&lt;/code&gt;, which might look a bit strange.&lt;/p&gt;

&lt;h3 id=&quot;32-bpe_train_step2_wrapperpyx&quot;&gt;3.2 bpe_train_step2_wrapper.pyx&lt;/h3&gt;

&lt;p&gt;Now, we’ll wrap the C++ functions into Python-callable functions. The main task here is parameter conversion, such as converting a Python &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict&lt;/code&gt; to a C++ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt;. Let’s look at the most direct implementation first.&lt;/p&gt;

&lt;h4 id=&quot;321-py_bpe_train_step2&quot;&gt;3.2.1 py_bpe_train_step2&lt;/h4&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef py_bpe_train_step2(int vocab_size,
                             pair_counts_py,
                             pair_strings_py,
                             vocabulary_py,
                             pair_wordids_py,
                             wordid_counts_py,
                             wordid_encodings_py,
                             merges_py):

    # 声明 C++ 容器
    cdef HashMap[pair[int, int], int, pair_hash] pair_counts_cpp
    cdef unordered_map[pair[int, int], vector[vector[int]], pair_hash] pair_strings_cpp
    cdef unordered_map[int, vector[int]] vocabulary_cpp
    cdef unordered_map[pair[int, int], unordered_set[int], pair_hash] pair_wordids_cpp
    cdef unordered_map[int, long long] wordid_counts_cpp
    cdef unordered_map[int, vector[int]] wordid_encodings_cpp
    cdef vector[pair[vector[int], vector[int]]] merges_cpp

 
    cdef pair[int, int] pair_key
    cdef vector[vector[int]] strings_value
    cdef vector[int] vector_value
    cdef unordered_set[int] set_value




    for p, count in pair_counts_py.items():
        pair_key.first = p[0]
        pair_key.second = p[1]
        pair_counts_cpp[pair_key] = count

    for p, string in pair_strings_py.items():
        pair_key.first = p[0]
        pair_key.second = p[1]
        strings_value.clear()
        value = [list(item) for item in string] 
        vector_value = value[0]
        strings_value.push_back(vector_value)
        vector_value = value[1]
        strings_value.push_back(vector_value)        
        pair_strings_cpp[pair_key] = strings_value     
    
    for k, v in vocabulary_py.items():
        value = list(v)
        vector_value = value
        vocabulary_cpp[k] = vector_value

    for p, wordids in pair_wordids_py.items():
        pair_key.first = p[0]
        pair_key.second = p[1]        
        set_value = wordids
        pair_wordids_cpp[pair_key] = set_value

    for k, v in wordid_counts_py.items():
        wordid_counts_cpp[k] = v

    for k, v in wordid_encodings_py.items():
        vector_value = v
        wordid_encodings_cpp[k] = vector_value

    # 调用 C++ 函数
    bpe_train_step2(vocab_size,
                    pair_counts_cpp,
                    pair_strings_cpp,
                    vocabulary_cpp,
                    pair_wordids_cpp,
                    wordid_counts_cpp,
                    wordid_encodings_cpp,
                    merges_cpp)

    return merges_cpp, vocabulary_cpp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Most of the code in this function converts Python variables to C++ variables before calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2&lt;/code&gt;. Let’s look at two typical examples:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    cdef HashMap[pair[int, int], int, pair_hash] pair_counts_cpp
    cdef pair[int, int] pair_key

    for p, count in pair_counts_py.items():
        pair_key.first = p[0]
        pair_key.second = p[1]
        pair_counts_cpp[pair_key] = count
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The key for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts_cpp&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair[int,int]&lt;/code&gt;, which is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::pair&amp;lt;int,int&amp;gt;&lt;/code&gt;. We can assign to its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;first&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;second&lt;/code&gt; members. We then use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;operator[]&lt;/code&gt; to insert, which is why we needed to declare &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;V&amp;amp; operator[](const K&amp;amp; key)&lt;/code&gt; earlier. The code iterates like a Python &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for&lt;/code&gt; loop.&lt;/p&gt;

&lt;p&gt;Let’s look at a more complex example:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    cdef unordered_map[pair[int, int], vector[vector[int]], pair_hash] pair_strings_cpp

    for p, string in pair_strings_py.items():
        pair_key.first = p[0]
        pair_key.second = p[1]
        strings_value.clear()
        value = [list(item) for item in string] 
        vector_value = value[0]
        strings_value.push_back(vector_value)
        vector_value = value[1]
        strings_value.push_back(vector_value)        
        pair_strings_cpp[pair_key] = strings_value   
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The key for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_strings_py&lt;/code&gt; is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tuple&lt;/code&gt;, and the value is also a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tuple&lt;/code&gt; with two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bytes&lt;/code&gt; elements. For example:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(111,110): (b&apos;o&apos;, b&apos;n&apos;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The corresponding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_strings_cpp&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unordered_map[pair[int, int], vector[vector[int]], pair_hash]&lt;/code&gt;, so we need to convert the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bytes&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list[int]&lt;/code&gt;. This is done with the following statements:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;value = [list(item) for item in string] 
vector_value = value[0]
strings_value.push_back(vector_value)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;First, a list comprehension converts &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tuple[bytes]&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list[list[int]]&lt;/code&gt;. Then, Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list[int]&lt;/code&gt; is converted to C++’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vector[int]&lt;/code&gt; through assignment, and finally pushed back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strings_value&lt;/code&gt;.&lt;/p&gt;

&lt;h4 id=&quot;322-py_bpe_train_step2_v2&quot;&gt;3.2.2 py_bpe_train_step2_v2&lt;/h4&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef py_bpe_train_step2_v2(int vocab_size,
                             vocabulary_py,
                             wordid_counts_py,
                             wordid_encodings_py,
                             merges_py):

    # 声明 C++ 容器
    cdef unordered_map[int, vector[int]] vocabulary_cpp
    cdef unordered_map[int, long long] wordid_counts_cpp
    cdef unordered_map[int, vector[int]] wordid_encodings_cpp
    cdef vector[pair[vector[int], vector[int]]] merges_cpp

 
    cdef pair[int, int] pair_key
    cdef vector[vector[int]] strings_value
    cdef vector[int] vector_value
    cdef unordered_set[int] set_value
 
    
    for k, v in vocabulary_py.items():
        value = list(v)
        vector_value = value
        vocabulary_cpp[k] = vector_value


    for k, v in wordid_counts_py.items():
        wordid_counts_cpp[k] = v

    for k, v in wordid_encodings_py.items():
        vector_value = v
        wordid_encodings_cpp[k] = vector_value

    # 调用 C++ 函数
    bpe_train_step2_v2(vocab_size,
                    vocabulary_cpp,
                    wordid_counts_cpp,
                    wordid_encodings_cpp,
                    merges_cpp)

    return merges_cpp, vocabulary_cpp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This version is similar to the previous one, but with three fewer parameters.&lt;/p&gt;

&lt;p&gt;Note: We return C++ variables &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merges_cpp&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vocabulary_cpp&lt;/code&gt; at the end. Their types are:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cdef unordered_map[int, vector[int]] vocabulary_cpp
cdef vector[pair[vector[int], vector[int]]] merges_cpp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When they are returned to Python, Cython automatically converts them to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict[int, list[int]]&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list[tuple[list[int], list[int]]]&lt;/code&gt;. We’ll need to convert the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list[int]&lt;/code&gt; back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bytes&lt;/code&gt; later.&lt;/p&gt;

&lt;p&gt;In fact, not just for return values, but also when we copy a Python variable to a C++ variable or vice versa, Cython automatically performs these common conversions:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Python type =&amp;gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;&lt;em&gt;C++ type&lt;/em&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;=&amp;gt; Python type&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bytes&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::string&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bytes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;iterable&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::vector&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;list&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;iterable&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::list&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;list&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;iterable&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::set&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;set&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;iterable&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::unordered_set&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;set&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;mapping&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::map&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;dict&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;mapping&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::unordered_map&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;dict&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;iterable (len 2)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::pair&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;tuple (len 2)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;complex&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::complex&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;complex&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;We can use this feature to simplify the variable conversions and get &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_opt&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef py_bpe_train_step2_opt(int vocab_size,
                             vocabulary_py,
                             wordid_counts_py,
                             wordid_encodings_py,
                             merges_py):

    # 声明 C++ 容器
    cdef unordered_map[int, vector[int]] vocabulary_cpp
    cdef unordered_map[int, long long] wordid_counts_cpp
    cdef unordered_map[int, vector[int]] wordid_encodings_cpp
    cdef vector[pair[vector[int], vector[int]]] merges_cpp

 
    vocabulary_cpp = vocabulary_py

    wordid_counts_cpp = wordid_counts_py

    wordid_encodings_cpp = wordid_encodings_py
    # 调用 C++ 函数
    bpe_train_step2_v2(vocab_size,
                    vocabulary_cpp,
                    wordid_counts_cpp,
                    wordid_encodings_cpp,
                    merges_cpp)

    return merges_cpp, vocabulary_cpp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here, we use just three assignment statements, and Cython automatically handles the conversion between Python and C++. The calls to other versions, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_v3&lt;/code&gt;, are identical to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_opt&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef py_bpe_train_step2_v3(int vocab_size,
                             vocabulary_py,
                             wordid_counts_py,
                             wordid_encodings_py,
                             merges_py):

    # 声明 C++ 容器
    cdef unordered_map[int, vector[int]] vocabulary_cpp
    cdef unordered_map[int, long long] wordid_counts_cpp
    cdef unordered_map[int, vector[int]] wordid_encodings_cpp
    cdef vector[pair[vector[int], vector[int]]] merges_cpp

 
    vocabulary_cpp = vocabulary_py

    wordid_counts_cpp = wordid_counts_py

    wordid_encodings_cpp = wordid_encodings_py
    # 调用 C++ 函数
    bpe_train_step2_v3(vocab_size,
                    vocabulary_cpp,
                    wordid_counts_cpp,
                    wordid_encodings_cpp,
                    merges_cpp)

    return merges_cpp, vocabulary_cpp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;33-modifying-setuppy&quot;&gt;3.3 Modifying setup.py&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;project_root = os.path.dirname(os.path.abspath(__file__))


ext_modules = [
    Extension(
        name=&quot;cs336_basics.bpe_train_step2_wrapper&quot;,
        sources=[&quot;cs336_basics/bpe_train_step2_wrapper.pyx&quot;],

        language=&quot;c++&quot;,
        #extra_compile_args=[&apos;-std=c++17&apos;, &apos;-O3&apos;],
        extra_compile_args=[&apos;-std=c++17&apos;],
        libraries=[&quot;bpe_train_step2&quot;],

        library_dirs=[f&quot;{project_root}/lib_bpe_train_step2/lib&quot;],
        runtime_library_dirs=[f&quot;{project_root}/lib_bpe_train_step2/lib&quot;],
        include_dirs=[f&quot;{project_root}/lib_bpe_train_step2/include&quot;,
                      f&quot;{project_root}/lib_bpe_train_step2/include/emhash&quot;],
    )
]

setup(
    packages=[&apos;cs336_basics&apos;],
    name=&apos;bpe_train_step2&apos;,
    ext_modules=cythonize(ext_modules),
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We need to compile &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_wrapper.pyx&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; specifies the module name, allowing Python to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import cs336_basics.bpe_train_step2_wrapper&lt;/code&gt;. Here, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cs336_basics&lt;/code&gt; is the package name, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2_wrapper&lt;/code&gt; is the module name.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sources&lt;/code&gt; specifies the source files to be compiled.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;language&lt;/code&gt; specifies the language of the module as C++.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extra_compile_args&lt;/code&gt; specifies additional compilation options, in this case &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;-std=c++17&apos;&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libraries&lt;/code&gt; lists the dependent libraries.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;library_dirs&lt;/code&gt; specifies the library location for compilation.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime_library_dirs&lt;/code&gt; specifies the library location for runtime.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;include_dirs&lt;/code&gt; specifies the header file location for compilation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To avoid hard-coding, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;project_root&lt;/code&gt; is set to the directory where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setup.py&lt;/code&gt; is located.&lt;/p&gt;

&lt;p&gt;These options ultimately result in the following C++ compiler commands (for my GCC environment; they may vary):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c++ -pthread -fno-strict-overflow -Wsign-compare -Wunreachable-code -DNDEBUG -g -O3 -Wall -fPIC -fPIC -Ics336_basics -I......codes/assignment1-basics-bpe/lib_bpe_train_step2/include -I......codes/assignment1-basics-bpe/lib_bpe_train_step2/include/emhash -I......codes/assignment1-basics-bpe/.venv/include -I.......local/share/uv/python/cpython-3.12.11-linux-x86_64-gnu/include/python3.12 -c cs336_basics/bpe_train_step2_wrapper.cpp -o build/temp.linux-x86_64-cpython-312/cs336_basics/bpe_train_step2_wrapper.o -std=c++17
c++ -pthread -fno-strict-overflow -Wsign-compare -Wunreachable-code -DNDEBUG -g -O3 -Wall -fPIC -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-312/cs336_basics/bpe_train_step2_wrapper.o -L......codes/assignment1-basics-bpe/lib_bpe_train_step2/lib -L.......local/share/uv/python/cpython-3.12.11-linux-x86_64-gnu/lib -Wl,--enable-new-dtags,-rpath,......codes/assignment1-basics-bpe/lib_bpe_train_step2/lib -lbpe_train_step2 -o build/lib.linux-x86_64-cpython-312/cs336_basics/bpe_train_step2_wrapper.cpython-312-x86_64-linux-gnu.so
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;34-using-it-in-python&quot;&gt;3.4 Using it in Python&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v9.py&quot;&gt;bpe_v9.py&lt;/a&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2&lt;/code&gt;; &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v10.py&quot;&gt;bpe_v10.py&lt;/a&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_v2&lt;/code&gt;; &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v10_v2.py&quot;&gt;bpe_v10_v2.py&lt;/a&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_opt&lt;/code&gt;; &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11.py&quot;&gt;bpe_v11.py&lt;/a&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_v3&lt;/code&gt;; &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_bytes.py&quot;&gt;bpe_v11_bytes.py&lt;/a&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_v3&lt;/code&gt;; &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_v2.py&quot;&gt;bpe_v11_v2.py&lt;/a&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_v4&lt;/code&gt;; &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_v3.py&quot;&gt;bpe_v11_v3.py&lt;/a&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_v5&lt;/code&gt;; &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_v3_bytes.py&quot;&gt;bpe_v11_v3_bytes.py&lt;/a&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_v5&lt;/code&gt;; &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_v4.py&quot;&gt;bpe_v11_v4.py&lt;/a&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_v6&lt;/code&gt;; &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_v4_bytes.py&quot;&gt;bpe_v11_v4_bytes.py&lt;/a&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_bpe_train_step2_v6&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Their code is mostly the same. Let’s look at &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11.py&quot;&gt;bpe_v11.py&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
        vocabulary = {i: bytes([i]) for i in range(N_BYTES)} # every byte
        for i, token in enumerate(special_tokens):
            vocabulary[N_BYTES + i] = token.encode(&apos;utf-8&apos;)
        size = N_BYTES + len(special_tokens)
        merges = []

        # initial word encodings are utf-8
        word_encodings = {}
        for word in word_counts:
            word_encodings[word] = list(word.encode(&apos;utf-8&apos;))

        word_ids = {word:id for id, word in enumerate(word_counts)}

        wordid_counts = {word_ids[word]:count for word, count in word_counts.items()}

        wordid_encodings = {word_ids[word]:encoding for word, encoding in word_encodings.items()}      


        merges_cpp, vocabulary_cpp = py_bpe_train_step2_v3(vocab_size, 
                             vocabulary,
                             wordid_counts,
                             wordid_encodings,
                             merges)


        vocabulary = {k:bytes(v) for k, v in vocabulary_cpp.items()}
        merges = [(bytes(arr[0]), bytes(arr[1])) for arr in merges_cpp]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Before the call, we need to convert string words to integer IDs. This mapping is stored in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;word_ids&lt;/code&gt;. We then use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;word_ids&lt;/code&gt; to convert &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;word_counts&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wordid_counts&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;word_encodings&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wordid_encodings&lt;/code&gt;. After the call, the returned &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merges_cpp&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vocabulary_cpp&lt;/code&gt; need to have their &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list[int]&lt;/code&gt; converted back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bytes&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;4-testing&quot;&gt;4. Testing&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Version&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Data&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Total Time (s)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Tokenization Time (s)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Merging Time (s)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v8_v3&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;897/899/951&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;395/399/395&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;442/438/493&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v9&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;831/816/867&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;400/401/390&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;prepare &amp;amp; convert: 93/86/94 py_bpe_train_step2: 330/320/374 c++:289/281/326&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v10&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;816/769/788&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;390/400/400&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;prepare &amp;amp; convert: 21/17/19 py_bpe_train_step2: 402/350/367 c++:338/296/309&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v10_v2&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;767/774/767&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;400/401/401&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;prepare &amp;amp; convert: 18/17/17 py_bpe_train_step2: 346/355/347 c++:292/298/294&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v10_v2&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;498/477/495&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;120/120/120&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;prepare &amp;amp; convert: 20/19/18 py_bpe_train_step2: 355/336/354 c++:299/282/298&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=32, num_merger=4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v11&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;350/340/354&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;120/120/120&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;prepare &amp;amp; convert: 21/19/19 py_bpe_train_step2: 207/199/212 c++:183/175/190&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=32, num_merger=4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v11_bytes&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;311/307/305&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;80/80/80&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;prepare &amp;amp; convert: 18/19/18 py_bpe_train_step2: 211/206/204 c++:189/183/182&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=64, num_merger=8, chunk_size 8mb&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v11_v2&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;362/350/338&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;130/120/120&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;prepare &amp;amp; convert: 19/18/19 py_bpe_train_step2: 210/210/197 c++:189/190/176&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=32, num_merger=4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v11_v3&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;269/274/270&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;120/120/120&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;prepare &amp;amp; convert: 18/19/18 py_bpe_train_step2: 129/133/129 c++: 106/109/106&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=32, num_merger=4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v11_v3_bytes&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;218/219/215&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;72/74/69&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;prepare &amp;amp; convert: 21/21/21 py_bpe_train_step2: 123/122/123 c++: 101/100/101&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=64, num_merger=8, chunk_size 8mb&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v11_v4&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;258/256/261&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;116/117/117&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;prepare &amp;amp; convert: 19/18/19 py_bpe_train_step2: 121/119/123 c++: 98/97/100&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=32, num_merger=4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bpe_v11_v4_bytes&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;open_web&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;210/206/207&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;71/69/70&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;prepare &amp;amp; convert: 20/18/19 py_bpe_train_step2: 117/117/117 c++: 95/96/95&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;num_counter=64, num_merger=8, chunk_size 8mb&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Comparing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v10&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v10_v2&lt;/code&gt;, the difference is between manually converting parameters in Python to C++ and letting Cython do it automatically. Automatic conversion is not only more convenient but also faster.&lt;/p&gt;

&lt;p&gt;Ultimately, with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v11_v4_bytes&lt;/code&gt; on 64 cores, the fastest training time is a little over 200 seconds, which is more than 100 times faster than our initial time of over ten hours!&lt;/p&gt;

&lt;h2 id=&quot;5-conclusion&quot;&gt;5. Conclusion&lt;/h2&gt;

&lt;p&gt;This concludes the series. Here is a brief summary and links to the corresponding articles:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/05/bpe-trainer-0_en/&quot;&gt;Part 0: Introduction&lt;/a&gt; Introduces the basic BPE training algorithm and related tasks, as well as the development environment.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/07/bpe-trainer-1_en/&quot;&gt;Part 1: The Simplest Implementation&lt;/a&gt; The simplest implementation of BPE training.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/08/bpe-trainer-2_en/&quot;&gt;Part 2: Optimized Algorithm&lt;/a&gt; Implements incremental updates for pair_counts.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/09/bpe-trainer-3_en/&quot;&gt;Part 3: Parallel Tokenization and Frequency Counting&lt;/a&gt; Uses multiprocessing to implement a multi-process parallel algorithm.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/10/bpe-trainer-4_en/&quot;&gt;Part 4: A Failed Parallel Optimization&lt;/a&gt; An attempt to parallelize the max pair calculation using multiple processes.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/12/bpe-trainer-5_en/&quot;&gt;Part 5: Implementing the Merge Algorithm in C++&lt;/a&gt; Implements a C++ merge algorithm equivalent to the Python version, and compares two ways of iterating through std::unordered_map.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/15/bpe-trainer-6_en/&quot;&gt;Part 6: Parallelizing the Max Pair Search with OpenMP&lt;/a&gt; Uses OpenMP to find the max pair in pair_counts in parallel.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/18/bpe-trainer-7_en/&quot;&gt;Part 7: Using Flat Hashmap to Replace std::unordered_map&lt;/a&gt; Uses flat hashmap to replace std::unordered_map.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/19/bpe-trainer-8_en/&quot;&gt;Part 8: Implementing Fine-Grained Updates&lt;/a&gt; Implements a fine-grained update algorithm for pair_counts using an inverted index.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/21/bpe-trainer-9_en/&quot;&gt;Part 9: Using a Heap to Find the Max Pair&lt;/a&gt; Uses a heap to find the max pair and improve performance.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/24/bpe-trainer-10_en/&quot;&gt;Part 10: Using Cython and PyPy for Acceleration&lt;/a&gt; Uses Cython and PyPy to accelerate Python code.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/25/bpe-trainer-11_en/&quot;&gt;Part 11: Wrapping C++ Code with Cython&lt;/a&gt; Wraps C++ code using Cython.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 25 Sep 2025 00:00:00 +0000</pubDate>
        <link>http://fancyerii.github.io/2025/09/25/bpe-trainer-11_en/</link>
        <guid isPermaLink="true">http://fancyerii.github.io/2025/09/25/bpe-trainer-11_en/</guid>
        
        <category>cs336</category>
        
        <category>bpe tokenizer</category>
        
        
      </item>
    
      <item>
        <title>动手实现和优化BPE Tokenizer的训练——第11部分：使用cython封装c++代码</title>
        <description>&lt;p&gt;本系列文章完成Stanford CS336作业1的一个子任务——实现BPE Tokenizer的高效训练算法。通过一系列优化，我们的算法在OpenWebText上的训练时间从最初的10多个小时优化到小于10分钟。本系列文章解释这一系列优化过程，包括：算法的优化，数据结构的优化，并行(openmp)优化，cython优化，用c++实现关键代码和c++库的cython集成等内容。本文是第十二篇，也是最后一篇，使用cython把之前的c++代码封装成扩展模块供python调用。&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#1-目标&quot; id=&quot;markdown-toc-1-目标&quot;&gt;1. 目标&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-把c代码封装成一个动态库&quot; id=&quot;markdown-toc-2-把c代码封装成一个动态库&quot;&gt;2. 把c++代码封装成一个动态库&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-头文件&quot; id=&quot;markdown-toc-21-头文件&quot;&gt;2.1 头文件&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-cmakeliststxt&quot; id=&quot;markdown-toc-22-cmakeliststxt&quot;&gt;2.2 CMakeLists.txt&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-bpe_train_step2&quot; id=&quot;markdown-toc-23-bpe_train_step2&quot;&gt;2.3 bpe_train_step2&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#24-bpe_train_step2_v2&quot; id=&quot;markdown-toc-24-bpe_train_step2_v2&quot;&gt;2.4 bpe_train_step2_v2&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#25-编译安装&quot; id=&quot;markdown-toc-25-编译安装&quot;&gt;2.5 编译安装&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-用cython封装成扩展模块&quot; id=&quot;markdown-toc-3-用cython封装成扩展模块&quot;&gt;3. 用cython封装成扩展模块&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-bpe_train_step2_wrapperpxd&quot; id=&quot;markdown-toc-31-bpe_train_step2_wrapperpxd&quot;&gt;3.1 bpe_train_step2_wrapper.pxd&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-bpe_train_step2_wrapperpyx&quot; id=&quot;markdown-toc-32-bpe_train_step2_wrapperpyx&quot;&gt;3.2 bpe_train_step2_wrapper.pyx&lt;/a&gt;        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#321-py_bpe_train_step2&quot; id=&quot;markdown-toc-321-py_bpe_train_step2&quot;&gt;3.2.1 py_bpe_train_step2&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#322-py_bpe_train_step2_v2&quot; id=&quot;markdown-toc-322-py_bpe_train_step2_v2&quot;&gt;3.2.2 py_bpe_train_step2_v2&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-修改setuppy&quot; id=&quot;markdown-toc-33-修改setuppy&quot;&gt;3.3 修改setup.py&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#34-在python里使用&quot; id=&quot;markdown-toc-34-在python里使用&quot;&gt;3.4 在python里使用&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-测试&quot; id=&quot;markdown-toc-4-测试&quot;&gt;4. 测试&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-总结&quot; id=&quot;markdown-toc-5-总结&quot;&gt;5. 总结&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;1-目标&quot;&gt;1. 目标&lt;/h2&gt;

&lt;p&gt;前面的文章我们已经做了很多探索，bpe训练可以分为两个阶段。第一个阶段是分词和统计，我们通过multiprocessing进行并行计算，实现的bpe_v3.py利用32核可以在120秒内完成。而读取bytes的bpe_v3_bytes_time.py可以使用64核在70秒内完成。第二个阶段是合并，python版本的bpe_v8_v3.py可以在500秒内完成，总时间是十多分钟。c++版本的合并最快的是bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp，它的合并时间是100秒左右。&lt;/p&gt;

&lt;p&gt;今天我们的目标就是实现一个最快的版本，通过cython封装c++代码成扩展模块，这样第二个阶段就可以调用这个模块。&lt;/p&gt;

&lt;h2 id=&quot;2-把c代码封装成一个动态库&quot;&gt;2. 把c++代码封装成一个动态库&lt;/h2&gt;

&lt;p&gt;我们在cppupdate里做了很多实验，现在我们需要把结果最好的一些版本封装成动态库，这样方便后续cython使用。&lt;/p&gt;

&lt;p&gt;我们会新建一个cppstep2的c++项目，它的完整代码在&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/tree/main/cppstep2&quot;&gt;cppstep2&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;根据cppupdate的实验结果，我选择了&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8.cpp&lt;/a&gt;、&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8_set.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8_set.cpp&lt;/a&gt;、&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8_set9.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8_set9.cpp&lt;/a&gt;、&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_emhash8_set9.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9.cpp&lt;/a&gt;和&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;选择emhash而不是absl的原因有二：一是它比较快；二是用它比较简单。我之前是git clone了emhash的完整代码，这里为了简化，我只是复制了需要的3个头文件：hash_set8.hpp, hash_set4.hpp和hash_table8.hpp。另外max_heap.h和max_heap.cpp也从cppupdate项目原封不动的复制了过来。&lt;/p&gt;

&lt;p&gt;我们的动态库主要就两个文件：头文件&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppstep2/bpe_train_step2.h&quot;&gt;bpe_train_step2.h&lt;/a&gt;和实现文件&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppstep2/bpe_train_step2.cpp&quot;&gt;bpe_train_step2.cpp&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&quot;21-头文件&quot;&gt;2.1 头文件&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#pragma once

#include &amp;lt;vector&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
#include &amp;lt;unordered_set&amp;gt;
#include &quot;emhash/hash_table8.hpp&quot;

struct pair_hash {
    template &amp;lt;class T1, class T2&amp;gt;
    std::size_t operator () (const std::pair&amp;lt;T1,T2&amp;gt; &amp;amp;p) const {
        auto h1 = std::hash&amp;lt;T1&amp;gt;{}(p.first);
        auto h2 = std::hash&amp;lt;T2&amp;gt;{}(p.second);

        return h1 ^ (h2 + 0x9e3779b9 + (h1 &amp;lt;&amp;lt; 6) + (h1 &amp;gt;&amp;gt; 2));
    }
};

void bpe_train_step2(int vocab_size, 
                emhash8::HashMap&amp;lt;std::pair&amp;lt;int, int&amp;gt;, int, pair_hash&amp;gt; &amp;amp; pair_counts, 
                std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;, pair_hash&amp;gt; &amp;amp; pair_strings, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::unordered_set&amp;lt;int&amp;gt;, pair_hash&amp;gt; &amp;amp; pair_wordids, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges);

void bpe_train_step2_v2(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges);

void bpe_train_step2_v3(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges);   
                
void bpe_train_step2_v4(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges);  

void bpe_train_step2_v5(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges);  

void bpe_train_step2_v6(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges); 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个头文件定义了bpe_train_step2_v2、bpe_train_step2_v3、bpe_train_step2_v4、bpe_train_step2_v5和bpe_train_step2_v6这5个函数，它们分别对应&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8.cpp&lt;/a&gt;、&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8_set.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8_set.cpp&lt;/a&gt;、&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8_set9.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8_set9.cpp&lt;/a&gt;、&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_emhash8_set9.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9.cpp&lt;/a&gt;和&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;bpe_train_step2是完全参考cppudate的实现，它的输入参数是6个。而后面我们会讲到pair_counts是wordid_counts是基于计算出来的，可以从python移植到c++，这样速度更快，而且少传递很多参数。所以bpe_train_step2_v2以及之后的版本都是只有5个参数。&lt;/p&gt;

&lt;h3 id=&quot;22-cmakeliststxt&quot;&gt;2.2 CMakeLists.txt&lt;/h3&gt;

&lt;p&gt;我们的目标是编译一个动态库，按照CMake的语法编写如下内容：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cmake_minimum_required(VERSION 3.20)

project(BPE_TRAIN_STEP2 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)


add_library(bpe_train_step2 SHARED bpe_train_step2.cpp max_heap.cpp)
target_include_directories(bpe_train_step2 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(bpe_train_step2 PUBLIC MY_LIBRARY_EXPORT)

target_include_directories(bpe_train_step2 PUBLIC
    &quot;${PROJECT_SOURCE_DIR}/emhash&quot;
)

install(TARGETS bpe_train_step2
        EXPORT bpe_train_step2_export_targets
        RUNTIME DESTINATION bin
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib)

install(FILES &quot;bpe_train_step2.h&quot; 
        DESTINATION include
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们通过add_library创建bpe_train_step2这个target。接着的target_compile_definitions好像是为了windows的兼容性定义的宏。我不懂windows，也没有windows环境测试，所以不知道这个项目能不能在windows下编译。&lt;/p&gt;

&lt;p&gt;然后的target_include_directories把emhash的头文件包含进来。最后是安装的时候把bpe_train_step2这个target(主要是libbpe_train_step2.so)和bpe_train_step2.h复制到合适的位置，后面我会讲到怎么安装。&lt;/p&gt;

&lt;h3 id=&quot;23-bpe_train_step2&quot;&gt;2.3 bpe_train_step2&lt;/h3&gt;

&lt;p&gt;首先我们按照cppupdate的方式集成，我们需要传入pair_counts等7个参数。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;void bpe_train_step2(int vocab_size, 
                emhash8::HashMap&amp;lt;std::pair&amp;lt;int, int&amp;gt;, int, pair_hash&amp;gt; &amp;amp; pair_counts, 
                std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;, pair_hash&amp;gt; &amp;amp; pair_strings, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::unordered_set&amp;lt;int&amp;gt;, pair_hash&amp;gt; &amp;amp; pair_wordids, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges){
    auto start = std::chrono::steady_clock::now();
    while(vocabulary.size() &amp;lt; vocab_size){
        int max_count = -1;
        std::pair&amp;lt;int, int&amp;gt; max_pair;
        std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt; max_strings;
        for(const auto&amp;amp; [pair, count] : pair_counts){
            if(count &amp;gt; max_count){
                max_count = count;
                max_pair = pair;
                max_strings = pair_strings[pair];
            }else if(count == max_count){
                std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt; strings = pair_strings[pair];
                ComparisonResult r1 = three_way_compare(strings[0], max_strings[0]);
                if(r1 == ComparisonResult::Greater){
                    max_count = count;
                    max_pair = pair;
                    max_strings = strings;
                }else if(r1 == ComparisonResult::Equal){
                    ComparisonResult r2 = three_way_compare(strings[1], max_strings[1]);
                    if(r2 == ComparisonResult::Greater){
                        max_count = count;
                        max_pair = pair;
                        max_strings = strings;                        
                    }
                }
            }
        }

        const std::vector&amp;lt;int&amp;gt;&amp;amp; bytes1 = vocabulary[max_pair.first];
        const std::vector&amp;lt;int&amp;gt;&amp;amp; bytes2 = vocabulary[max_pair.second];
        std::vector&amp;lt;int&amp;gt; merge_bytes;
        merge_bytes.reserve(bytes1.size() + bytes2.size());
        merge_bytes.insert(merge_bytes.end(), bytes1.begin(), bytes1.end());
        merge_bytes.insert(merge_bytes.end(), bytes2.begin(), bytes2.end());

        int size = vocabulary.size();
        vocabulary[size] = merge_bytes;

        auto&amp;amp; affected_words = pair_wordids[max_pair];

        
        updated_affected_word_count(max_pair, affected_words, wordid_encodings, wordid_counts,
                                    pair_counts, pair_wordids, size, pair_strings, vocabulary);
  
        merges.push_back({bytes1, bytes2});


    }
    auto end = std::chrono::steady_clock::now();
    auto duration = std::chrono::duration_cast&amp;lt;std::chrono::milliseconds&amp;gt;(end - start);
    std::cout &amp;lt;&amp;lt; &quot;bpe_train_step2: &quot; &amp;lt;&amp;lt; duration.count() &amp;lt;&amp;lt; &quot;ms.&quot; &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;代码完全是复制&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8.cpp&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&quot;24-bpe_train_step2_v2&quot;&gt;2.4 bpe_train_step2_v2&lt;/h3&gt;

&lt;p&gt;如果读者仔细看过之前的时间统计，会发现总时间会比”统计词频时间” + “合并时间” 要多五六十秒。这个时间就是调用&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v3.py#L54&quot;&gt;BPE_Trainer._count_pairs&lt;/a&gt;的时间：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    @staticmethod    
    def _count_pairs(word_counts, word_encodings, pair_strings, vocabulary, pair_to_words):
        pair_counts = defaultdict(int)
        for word, count in word_counts.items():
            encoding = word_encodings[word]
            for i in range(0, len(encoding) - 1):
                pair = encoding[i], encoding[i + 1]
                pair_counts[pair] += count
                if pair not in pair_strings:
                    pair_strings[pair] = (vocabulary[pair[0]], vocabulary[pair[1]])

                pair_to_words[pair].add(word)

        return pair_counts
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这段代码的作用就是根据word_counts统计pair_counts，构建初始的倒排索引pair_to_words以及pair_strings。它的输入参数是word_counts, word_encodings和vocabulary。这段代码完全可以在c++里实现，这样一来可以加速，二来可以减少python和c++之间的参数传递。&lt;/p&gt;

&lt;p&gt;因此bpe_train_step2_v2的输入参数就可以减少三个：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;void bpe_train_step2_v2(int vocab_size,          
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; vocabulary, 
                const std::unordered_map&amp;lt;int, long long&amp;gt; &amp;amp; wordid_counts, 
                std::unordered_map&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp; wordid_encodings, 
                std::vector&amp;lt;std::pair&amp;lt;std::vector&amp;lt;int&amp;gt;, std::vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; &amp;amp; merges){
    std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;, pair_hash&amp;gt; pair_strings;
    emhash8::HashMap&amp;lt;std::pair&amp;lt;int, int&amp;gt;, int, pair_hash&amp;gt;  pair_counts;
    std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::unordered_set&amp;lt;int&amp;gt;, pair_hash&amp;gt;  pair_wordids;
    
    std::pair&amp;lt;int, int&amp;gt; pair;
    for(const auto&amp;amp; [wordid, count] : wordid_counts){
        const auto&amp;amp; word_tokens = wordid_encodings[wordid];
        for(int i = 0; i &amp;lt; word_tokens.size() - 1; ++i){
            pair.first = word_tokens[i];
            pair.second = word_tokens[i + 1];
            pair_counts[pair] += count;
            if (pair_strings.find(pair) == pair_strings.end()) {
                pair_strings[pair] = {vocabulary[pair.first], vocabulary[pair.second]};
            }
            pair_wordids[pair].insert(wordid);
        }
    }
    

    bpe_train_step2(vocab_size, pair_counts, pair_strings, vocabulary,
                    pair_wordids, wordid_counts, wordid_encodings, merges);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;bpe_train_step2_v2这个函数首先根据wordid_counts、wordid_encodings和vocabulary计算得到pair_counts、pair_strings和pair_wordids，最后还是调用bpe_train_step2。&lt;/p&gt;

&lt;p&gt;bpe_train_step2_v3和之后的版本接口与bpe_train_step2_v2完全一样，代码也是复制相应的cppupdate里的代码，这里就不赘述了。&lt;/p&gt;

&lt;h3 id=&quot;25-编译安装&quot;&gt;2.5 编译安装&lt;/h3&gt;

&lt;p&gt;使用如下命令编译安装：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cd cppstep2
mkdir build &amp;amp;&amp;amp; cd build
cmake -D CMAKE_INSTALL_PREFIX=../../lib_bpe_train_step2/  -D CMAKE_BUILD_TYPE=Release ..
cmake --build . -- -j8
# 如果是gcc的话可以直接make -j8
cmake --install .  
# 或者make install
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;由于我们不希望把这个库安装到系统的路径，比如/usr/local下，因为这样的安装需要root权限。而且不同系统的路径不相同，也不便于cython的集成。所以我这里把它安装到../../lib_bpe_train_step2/，也就是项目根目录assignment1-basics-bpe之下的lib_bpe_train_step2：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ ls assignment1-basics-bpe

cppstep2/
cppupdate/
cs336_basics/
data/
lib_bpe_train_step2/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;安装后的lib_bpe_train_step2为：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;lib_bpe_train_step2$ tree
.
├── include
│   ├── bpe_train_step2.h
│   └── emhash
│       └── hash_table8.hpp
└── lib
    └── libbpe_train_step2.so
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;除了bpe_train_step2.h，hash_table8.hpp也被包含了进来，因为bpe_train_step2.h要用到它。这主要是因为bpe_train_step2的声明要用到它。如果我们只保留bpe_train_step2_v2及其之后的版本，那么这个头文件也不需要安装(但是我们编译还是需要emhash下的所有头文件)。&lt;/p&gt;

&lt;h2 id=&quot;3-用cython封装成扩展模块&quot;&gt;3. 用cython封装成扩展模块&lt;/h2&gt;

&lt;p&gt;对于cython封装c++库不熟悉的读者可以参考&lt;a href=&quot;https://cython.readthedocs.io/en/stable/src/userguide/wrapping_CPlusPlus.html&quot;&gt;Using C++ in Cython&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;我们需要两个文件：&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_train_step2_wrapper.pxd&quot;&gt;bpe_train_step2_wrapper.pxd&lt;/a&gt;和&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_train_step2_wrapper.pyx&quot;&gt;bpe_train_step2_wrapper.pyx&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&quot;31-bpe_train_step2_wrapperpxd&quot;&gt;3.1 bpe_train_step2_wrapper.pxd&lt;/h3&gt;

&lt;p&gt;首先来看pxd文件：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# distutils: language = c++

# 导入 C++ 标准库类型
from libcpp.utility cimport pair
from libcpp.vector cimport vector
from libcpp.unordered_map cimport unordered_map
from libcpp.unordered_set cimport unordered_set


cdef extern from &quot;../lib_bpe_train_step2/include/bpe_train_step2.h&quot; :
    cppclass pair_hash:
        pass

cdef extern from &quot;../lib_bpe_train_step2/include/emhash/hash_table8.hpp&quot; namespace &quot;emhash8&quot;:
    cppclass HashMap[K, V, H]:
        #ValueT&amp;amp; operator[](const KeyT&amp;amp; key) noexcept
        V&amp;amp; operator[](const K&amp;amp; key)


cdef extern from &quot;../lib_bpe_train_step2/include/bpe_train_step2.h&quot;:
    void bpe_train_step2(int vocab_size,
                         HashMap[pair[int, int], int, pair_hash] &amp;amp; pair_counts,
                         unordered_map[pair[int, int], vector[vector[int]], pair_hash] &amp;amp; pair_strings,
                         unordered_map[int, vector[int]] &amp;amp; vocabulary,
                         unordered_map[pair[int, int], unordered_set[int], pair_hash] &amp;amp; pair_wordids,
                         const unordered_map[int, long long] &amp;amp; wordid_counts,
                         unordered_map[int, vector[int]] &amp;amp; wordid_encodings,
                         vector[pair[vector[int], vector[int]]] &amp;amp; merges) except +
    
    void bpe_train_step2_v2(int vocab_size,
                         unordered_map[int, vector[int]] &amp;amp; vocabulary,
                         const unordered_map[int, long long] &amp;amp; wordid_counts,
                         unordered_map[int, vector[int]] &amp;amp; wordid_encodings,
                         vector[pair[vector[int], vector[int]]] &amp;amp; merges) except +


&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;第一行用来告诉 distutils(deprecated)或setuptools 模块：在这个脚本中，你正在构建一个 C++ 扩展模块，而不是默认的 C 语言扩展。&lt;/p&gt;

&lt;p&gt;接下来4行是我们要用到c++标准库的std::vector、std::pair、std::unordered_map和std::unordered_set。cython把这些常见的标准库都封装到libcpp下了，我们只需要用cimport(类似python的import，不过这是编译时而不是运行时的import)导入即可使用。要查看哪些c++标准库可以在cython直接使用可以参考&lt;a href=&quot;https://github.com/cython/cython/tree/master/Cython/Includes/libcpp&quot;&gt;这里&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;接下来用cppclass声明编译时要用到的pair_hash，因为我们使用lib_bpe_train_step2只需要知道pair_hash这个符号即可，它的内容我们不需要，所以它的内容就是一行pass。注意使用cdef extern from的路径是”../lib_bpe_train_step2/include/bpe_train_step2.h”，这就要求lib_bpe_train_step2按照前面的步骤安装到了合适的位置。&lt;/p&gt;

&lt;p&gt;接下来是声明emhash8::HashMap，在cdef extern from后有一个”namespace emhash8”，这样cython知道HashMap是在emhash8这个namespace下。cppclass HashMap[K, V, H]说明HashMap是一个模板类，分别代表Key/Value/Hash函数。此外后面的pyx文件里我们会用到operator[]，所以我们也需要声明这个重载运算符的原型。&lt;/p&gt;

&lt;p&gt;最后就是声明bpe_train_step2这些函数了，cython和c++的语法类似，只不过&amp;lt;&amp;gt;要改成[]，看起来有点别扭。&lt;/p&gt;

&lt;h3 id=&quot;32-bpe_train_step2_wrapperpyx&quot;&gt;3.2 bpe_train_step2_wrapper.pyx&lt;/h3&gt;

&lt;p&gt;接下来就是把c++的函数封装成python可以调用的函数，这里主要做的就是参数的转换，比如把python的dict变成c++的std::unordered_map。我们先看最直接的实现。&lt;/p&gt;

&lt;h4 id=&quot;321-py_bpe_train_step2&quot;&gt;3.2.1 py_bpe_train_step2&lt;/h4&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef py_bpe_train_step2(int vocab_size,
                             pair_counts_py,
                             pair_strings_py,
                             vocabulary_py,
                             pair_wordids_py,
                             wordid_counts_py,
                             wordid_encodings_py,
                             merges_py):

    # 声明 C++ 容器
    cdef HashMap[pair[int, int], int, pair_hash] pair_counts_cpp
    cdef unordered_map[pair[int, int], vector[vector[int]], pair_hash] pair_strings_cpp
    cdef unordered_map[int, vector[int]] vocabulary_cpp
    cdef unordered_map[pair[int, int], unordered_set[int], pair_hash] pair_wordids_cpp
    cdef unordered_map[int, long long] wordid_counts_cpp
    cdef unordered_map[int, vector[int]] wordid_encodings_cpp
    cdef vector[pair[vector[int], vector[int]]] merges_cpp

 
    cdef pair[int, int] pair_key
    cdef vector[vector[int]] strings_value
    cdef vector[int] vector_value
    cdef unordered_set[int] set_value




    for p, count in pair_counts_py.items():
        pair_key.first = p[0]
        pair_key.second = p[1]
        pair_counts_cpp[pair_key] = count

    for p, string in pair_strings_py.items():
        pair_key.first = p[0]
        pair_key.second = p[1]
        strings_value.clear()
        value = [list(item) for item in string] 
        vector_value = value[0]
        strings_value.push_back(vector_value)
        vector_value = value[1]
        strings_value.push_back(vector_value)        
        pair_strings_cpp[pair_key] = strings_value     
    
    for k, v in vocabulary_py.items():
        value = list(v)
        vector_value = value
        vocabulary_cpp[k] = vector_value

    for p, wordids in pair_wordids_py.items():
        pair_key.first = p[0]
        pair_key.second = p[1]        
        set_value = wordids
        pair_wordids_cpp[pair_key] = set_value

    for k, v in wordid_counts_py.items():
        wordid_counts_cpp[k] = v

    for k, v in wordid_encodings_py.items():
        vector_value = v
        wordid_encodings_cpp[k] = vector_value

    # 调用 C++ 函数
    bpe_train_step2(vocab_size,
                    pair_counts_cpp,
                    pair_strings_cpp,
                    vocabulary_cpp,
                    pair_wordids_cpp,
                    wordid_counts_cpp,
                    wordid_encodings_cpp,
                    merges_cpp)

    return merges_cpp, vocabulary_cpp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个函数前面的大部分代码都是把python的变量转换成c++的变量，然后调用bpe_train_step2。我们来看两个典型的例子：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    cdef HashMap[pair[int, int], int, pair_hash] pair_counts_cpp
    cdef pair[int, int] pair_key

    for p, count in pair_counts_py.items():
        pair_key.first = p[0]
        pair_key.second = p[1]
        pair_counts_cpp[pair_key] = count
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;pair_counts_cpp的key是pair[int,int]，也就是std::pair&amp;lt;int,int&amp;gt;，我们可以通过first和second对它赋值。最后通过运算符[]对它进行插入，这也是之前我们需要声明V&amp;amp; operator&lt;a href=&quot;const K&amp;amp; key&quot;&gt;&lt;/a&gt;的原因。我们可以发现上面的代码是类似python的for遍历。&lt;/p&gt;

&lt;p&gt;再看一个复杂一点的：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    cdef unordered_map[pair[int, int], vector[vector[int]], pair_hash] pair_strings_cpp

    for p, string in pair_strings_py.items():
        pair_key.first = p[0]
        pair_key.second = p[1]
        strings_value.clear()
        value = [list(item) for item in string] 
        vector_value = value[0]
        strings_value.push_back(vector_value)
        vector_value = value[1]
        strings_value.push_back(vector_value)        
        pair_strings_cpp[pair_key] = strings_value   
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;pair_strings_py的key是一个tuple，value也是一个tuple，这个tuple有2个元素，每个都是一个bytes。说起来很费劲，我们看一个例子：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(111,110): (b&apos;o&apos;, b&apos;n&apos;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们对应的pair_strings_cpp是unordered_map[pair[int, int], vector[vector[int]], pair_hash]，所以需要把bytes变成list[int]。这是通过下面的语句实现：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;value = [list(item) for item in string] 
vector_value = value[0]
strings_value.push_back(vector_value)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;首先是用列表推导把tuple[bytes]变成list[[list[int]]]，然后通过赋值把python的list[int]变成c++的vector[int]，最后push_back到strings_value里。&lt;/p&gt;

&lt;h4 id=&quot;322-py_bpe_train_step2_v2&quot;&gt;3.2.2 py_bpe_train_step2_v2&lt;/h4&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef py_bpe_train_step2_v2(int vocab_size,
                             vocabulary_py,
                             wordid_counts_py,
                             wordid_encodings_py,
                             merges_py):

    # 声明 C++ 容器
    cdef unordered_map[int, vector[int]] vocabulary_cpp
    cdef unordered_map[int, long long] wordid_counts_cpp
    cdef unordered_map[int, vector[int]] wordid_encodings_cpp
    cdef vector[pair[vector[int], vector[int]]] merges_cpp

 
    cdef pair[int, int] pair_key
    cdef vector[vector[int]] strings_value
    cdef vector[int] vector_value
    cdef unordered_set[int] set_value
 
    
    for k, v in vocabulary_py.items():
        value = list(v)
        vector_value = value
        vocabulary_cpp[k] = vector_value


    for k, v in wordid_counts_py.items():
        wordid_counts_cpp[k] = v

    for k, v in wordid_encodings_py.items():
        vector_value = v
        wordid_encodings_cpp[k] = vector_value

    # 调用 C++ 函数
    bpe_train_step2_v2(vocab_size,
                    vocabulary_cpp,
                    wordid_counts_cpp,
                    wordid_encodings_cpp,
                    merges_cpp)

    return merges_cpp, vocabulary_cpp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个版本和之前差不多，只不过少了3个参数。&lt;/p&gt;

&lt;p&gt;注意：最后我们返回的是c++的变量merges_cpp, vocabulary_cpp，它们的类型是：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cdef unordered_map[int, vector[int]] vocabulary_cpp
cdef vector[pair[vector[int], vector[int]]] merges_cpp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;返回到python时，cython会自动把它转换成dict[int,list[int]]和list[tuple[list[int], list[int]]]。我们后面需要再把list[int]变成bytes。&lt;/p&gt;

&lt;p&gt;其实不只是返回值，我们把一个python变量复制给一个c++变量或者把一个c++变量复制给python时cython也会自动的做这些常见的转换：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Python type =&amp;gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;&lt;em&gt;C++ type&lt;/em&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;=&amp;gt; Python type&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bytes&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::string&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bytes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;iterable&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::vector&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;list&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;iterable&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::list&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;list&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;iterable&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::set&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;set&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;iterable&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::unordered_set&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;set&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;mapping&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::map&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;dict&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;mapping&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::unordered_map&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;dict&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;iterable (len 2)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::pair&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;tuple (len 2)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;complex&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;std::complex&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;complex&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;我们可以用这个特性来简化变量之间的转换，得到py_bpe_train_step2_opt：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef py_bpe_train_step2_opt(int vocab_size,
                             vocabulary_py,
                             wordid_counts_py,
                             wordid_encodings_py,
                             merges_py):

    # 声明 C++ 容器
    cdef unordered_map[int, vector[int]] vocabulary_cpp
    cdef unordered_map[int, long long] wordid_counts_cpp
    cdef unordered_map[int, vector[int]] wordid_encodings_cpp
    cdef vector[pair[vector[int], vector[int]]] merges_cpp

 
    vocabulary_cpp = vocabulary_py

    wordid_counts_cpp = wordid_counts_py

    wordid_encodings_cpp = wordid_encodings_py
    # 调用 C++ 函数
    bpe_train_step2_v2(vocab_size,
                    vocabulary_cpp,
                    wordid_counts_cpp,
                    wordid_encodings_cpp,
                    merges_cpp)

    return merges_cpp, vocabulary_cpp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里我们直接用3条复制语句，cython自动就会帮我们在python和c++之间进行转换。其它版本的调用都是和py_bpe_train_step2_opt一样，比如py_bpe_train_step2_v3：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef py_bpe_train_step2_v3(int vocab_size,
                             vocabulary_py,
                             wordid_counts_py,
                             wordid_encodings_py,
                             merges_py):

    # 声明 C++ 容器
    cdef unordered_map[int, vector[int]] vocabulary_cpp
    cdef unordered_map[int, long long] wordid_counts_cpp
    cdef unordered_map[int, vector[int]] wordid_encodings_cpp
    cdef vector[pair[vector[int], vector[int]]] merges_cpp

 
    vocabulary_cpp = vocabulary_py

    wordid_counts_cpp = wordid_counts_py

    wordid_encodings_cpp = wordid_encodings_py
    # 调用 C++ 函数
    bpe_train_step2_v3(vocab_size,
                    vocabulary_cpp,
                    wordid_counts_cpp,
                    wordid_encodings_cpp,
                    merges_cpp)

    return merges_cpp, vocabulary_cpp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;33-修改setuppy&quot;&gt;3.3 修改setup.py&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;project_root = os.path.dirname(os.path.abspath(__file__))


ext_modules = [
    Extension(
        name=&quot;cs336_basics.bpe_train_step2_wrapper&quot;,
        sources=[&quot;cs336_basics/bpe_train_step2_wrapper.pyx&quot;],

        language=&quot;c++&quot;,
        #extra_compile_args=[&apos;-std=c++17&apos;, &apos;-O3&apos;],
        extra_compile_args=[&apos;-std=c++17&apos;],
        libraries=[&quot;bpe_train_step2&quot;],

        library_dirs=[f&quot;{project_root}/lib_bpe_train_step2/lib&quot;],
        runtime_library_dirs=[f&quot;{project_root}/lib_bpe_train_step2/lib&quot;],
        include_dirs=[f&quot;{project_root}/lib_bpe_train_step2/include&quot;,
                      f&quot;{project_root}/lib_bpe_train_step2/include/emhash&quot;],
    )
]

setup(
    packages=[&apos;cs336_basics&apos;],
    name=&apos;bpe_train_step2&apos;,
    ext_modules=cythonize(ext_modules),
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们需要编译bpe_train_step2_wrapper.pyx。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;name是指定模块的名字，这样python里可以import cs336_basics.bpe_train_step2_wrapper。这里的cs336_basics是package的名字，bpe_train_step2_wrapper是模块名。&lt;/li&gt;
  &lt;li&gt;sources指定要编译的源代码&lt;/li&gt;
  &lt;li&gt;language指定模块是c++语言&lt;/li&gt;
  &lt;li&gt;extra_compile_args指定额外的编译选项，这里指定了’-std=c++17’&lt;/li&gt;
  &lt;li&gt;libraries是依赖的库&lt;/li&gt;
  &lt;li&gt;library_dirs指定编译时库的位置&lt;/li&gt;
  &lt;li&gt;runtime_library_dirs指定运行时库的位置&lt;/li&gt;
  &lt;li&gt;include_dirs指定编译时的头文件位置&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为了避免硬编码，project_root为setup.py文件所在的目录。&lt;/p&gt;

&lt;p&gt;这些选项最终会使得c++编译器的编译和链接命令为(我的gcc环境，不同环境可能会有差异)：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c++ -pthread -fno-strict-overflow -Wsign-compare -Wunreachable-code -DNDEBUG -g -O3 -Wall -fPIC -fPIC -Ics336_basics -I......codes/assignment1-basics-bpe/lib_bpe_train_step2/include -I......codes/assignment1-basics-bpe/lib_bpe_train_step2/include/emhash -I......codes/assignment1-basics-bpe/.venv/include -I.......local/share/uv/python/cpython-3.12.11-linux-x86_64-gnu/include/python3.12 -c cs336_basics/bpe_train_step2_wrapper.cpp -o build/temp.linux-x86_64-cpython-312/cs336_basics/bpe_train_step2_wrapper.o -std=c++17
c++ -pthread -fno-strict-overflow -Wsign-compare -Wunreachable-code -DNDEBUG -g -O3 -Wall -fPIC -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-312/cs336_basics/bpe_train_step2_wrapper.o -L......codes/assignment1-basics-bpe/lib_bpe_train_step2/lib -L.......local/share/uv/python/cpython-3.12.11-linux-x86_64-gnu/lib -Wl,--enable-new-dtags,-rpath,......codes/assignment1-basics-bpe/lib_bpe_train_step2/lib -lbpe_train_step2 -o build/lib.linux-x86_64-cpython-312/cs336_basics/bpe_train_step2_wrapper.cpython-312-x86_64-linux-gnu.so
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;34-在python里使用&quot;&gt;3.4 在python里使用&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v9.py&quot;&gt;bpe_v9.py&lt;/a&gt;调用的是py_bpe_train_step2函数；&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v10.py&quot;&gt;bpe_v10.py&lt;/a&gt;调用的是py_bpe_train_step2_v2；&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v10_v2.py&quot;&gt;bpe_v10_v2.py&lt;/a&gt;调用的是py_bpe_train_step2_opt；&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11.py&quot;&gt;bpe_v11.py&lt;/a&gt;调用的是py_bpe_train_step2_v3；&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_bytes.py&quot;&gt;bpe_v11_bytes.py&lt;/a&gt;调用的是py_bpe_train_step2_v3；&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_v2.py&quot;&gt;bpe_v11_v2.py&lt;/a&gt;调用的是py_bpe_train_step2_v4；&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_v3.py&quot;&gt;bpe_v11_v3.py&lt;/a&gt;调用的是py_bpe_train_step2_v5；&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_v3_bytes.py&quot;&gt;bpe_v11_v3_bytes.py&lt;/a&gt;调用的是py_bpe_train_step2_v5；&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_v4.py&quot;&gt;bpe_v11_v4.py&lt;/a&gt;调用的是py_bpe_train_step2_v6；&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11_v4_bytes.py&quot;&gt;bpe_v11_v4_bytes.py&lt;/a&gt;调用的是py_bpe_train_step2_v6。&lt;/p&gt;

&lt;p&gt;它们的代码基本相同，这里看一下&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v11.py&quot;&gt;bpe_v11.py&lt;/a&gt;：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
        vocabulary = {i: bytes([i]) for i in range(N_BYTES)} # every byte
        for i, token in enumerate(special_tokens):
            vocabulary[N_BYTES + i] = token.encode(&apos;utf-8&apos;)
        size = N_BYTES + len(special_tokens)
        merges = []

        # initial word encodings are utf-8
        word_encodings = {}
        for word in word_counts:
            word_encodings[word] = list(word.encode(&apos;utf-8&apos;))

        word_ids = {word:id for id, word in enumerate(word_counts)}

        wordid_counts = {word_ids[word]:count for word, count in word_counts.items()}

        wordid_encodings = {word_ids[word]:encoding for word, encoding in word_encodings.items()}      


        merges_cpp, vocabulary_cpp = py_bpe_train_step2_v3(vocab_size, 
                             vocabulary,
                             wordid_counts,
                             wordid_encodings,
                             merges)


        vocabulary = {k:bytes(v) for k, v in vocabulary_cpp.items()}
        merges = [(bytes(arr[0]), bytes(arr[1])) for arr in merges_cpp]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在调用之前，我们需要把str的word变成int的id，这个映射关系保存在word_ids。然后利用word_ids把word_counts变成wordid_counts，把word_encodings变成wordid_encodings。调用后得到的merges_cpp, vocabulary_cpp需要把list[int]转换成bytes。&lt;/p&gt;

&lt;h2 id=&quot;4-测试&quot;&gt;4. 测试&lt;/h2&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;数据&lt;/td&gt;
      &lt;td&gt;总时间(s)&lt;/td&gt;
      &lt;td&gt;统计词频时间(s)&lt;/td&gt;
      &lt;td&gt;合并时间(s)&lt;/td&gt;
      &lt;td&gt;其它&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8_v3&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;897/899/951&lt;/td&gt;
      &lt;td&gt;395/399/395&lt;/td&gt;
      &lt;td&gt;442/438/493&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v9&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;831/816/867&lt;/td&gt;
      &lt;td&gt;400/401/390&lt;/td&gt;
      &lt;td&gt;prepare &amp;amp; convert: 93/86/94 py_bpe_train_step2: 330/320/374 c++:289/281/326&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v10&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;816/769/788&lt;/td&gt;
      &lt;td&gt;390/400/400&lt;/td&gt;
      &lt;td&gt;prepare &amp;amp; convert: 21/17/19 py_bpe_train_step2: 402/350/367 c++:338/296/309&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v10_v2&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;767/774/767&lt;/td&gt;
      &lt;td&gt;400/401/401&lt;/td&gt;
      &lt;td&gt;prepare &amp;amp; convert: 18/17/17 py_bpe_train_step2: 346/355/347 c++:292/298/294&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v10_v2&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;498/477/495&lt;/td&gt;
      &lt;td&gt;120/120/120&lt;/td&gt;
      &lt;td&gt;prepare &amp;amp; convert: 20/19/18 py_bpe_train_step2: 355/336/354 c++:299/282/298&lt;/td&gt;
      &lt;td&gt;num_counter=32, num_merger=4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v11&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;350/340/354&lt;/td&gt;
      &lt;td&gt;120/120/120&lt;/td&gt;
      &lt;td&gt;prepare &amp;amp; convert: 21/19/19 py_bpe_train_step2: 207/199/212 c++:183/175/190&lt;/td&gt;
      &lt;td&gt;num_counter=32, num_merger=4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v11_bytes&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;311/307/305&lt;/td&gt;
      &lt;td&gt;80/80/80&lt;/td&gt;
      &lt;td&gt;prepare &amp;amp; convert: 18/19/18 py_bpe_train_step2: 211/206/204 c++:189/183/182&lt;/td&gt;
      &lt;td&gt;num_counter=64, num_merger=8, chunk_size 8mb&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v11_v2&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;362/350/338&lt;/td&gt;
      &lt;td&gt;130/120/120&lt;/td&gt;
      &lt;td&gt;prepare &amp;amp; convert: 19/18/19 py_bpe_train_step2: 210/210/197 c++:189/190/176&lt;/td&gt;
      &lt;td&gt;num_counter=32, num_merger=4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v11_v3&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;269/274/270&lt;/td&gt;
      &lt;td&gt;120/120/120&lt;/td&gt;
      &lt;td&gt;prepare &amp;amp; convert: 18/19/18 py_bpe_train_step2: 129/133/129 c++: 106/109/106&lt;/td&gt;
      &lt;td&gt;num_counter=32, num_merger=4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v11_v3_bytes&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;218/219/215&lt;/td&gt;
      &lt;td&gt;72/74/69&lt;/td&gt;
      &lt;td&gt;prepare &amp;amp; convert: 21/21/21 py_bpe_train_step2: 123/122/123 c++: 101/100/101&lt;/td&gt;
      &lt;td&gt;num_counter=64, num_merger=8, chunk_size 8mb&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v11_v4&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;258/256/261&lt;/td&gt;
      &lt;td&gt;116/117/117&lt;/td&gt;
      &lt;td&gt;prepare &amp;amp; convert: 19/18/19 py_bpe_train_step2: 121/119/123 c++: 98/97/100&lt;/td&gt;
      &lt;td&gt;num_counter=32, num_merger=4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v11_v4_bytes&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;210/206/207&lt;/td&gt;
      &lt;td&gt;71/69/70&lt;/td&gt;
      &lt;td&gt;prepare &amp;amp; convert: 20/18/19 py_bpe_train_step2: 117/117/117 c++: 95/96/95&lt;/td&gt;
      &lt;td&gt;num_counter=64, num_merger=8, chunk_size 8mb&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;对比bpe_v10和bpe_v10_v2，它们的差别一是手动在python和c++之间转换参数一是cython自动转换。自动转换不仅更方便，而且更快。&lt;/p&gt;

&lt;p&gt;最终我们使用64核bpe_v11_v4_bytes最快的训练时间是200多秒，这比最初的十多个小时快了100多倍！&lt;/p&gt;

&lt;h2 id=&quot;5-总结&quot;&gt;5. 总结&lt;/h2&gt;

&lt;p&gt;这就是本系列文章的全部内容。下面是简要的总结和对应文章的链接：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/05/bpe-trainer-0/&quot;&gt;第0部分：简介&lt;/a&gt; 介绍bpe训练的基本算法和相关任务，并且介绍开发环境。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/07/bpe-trainer-1/&quot;&gt;第1部分：最简单实现&lt;/a&gt; bpe训练最简单的实现。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/08/bpe-trainer-2/&quot;&gt;第2部分：优化算法&lt;/a&gt; 实现pair_counts的增量更新。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/09/bpe-trainer-3/&quot;&gt;第3部分：并行分词和统计词频&lt;/a&gt; 使用multiprocessing实现多进程并行算法。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/10/bpe-trainer-4/&quot;&gt;第4部分：一次失败的并行优化&lt;/a&gt; 尝试用多进程并行计算max pair。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/12/bpe-trainer-5/&quot;&gt;第5部分：用C++实现Merge算法&lt;/a&gt; 用C++实现和Python等价的merge算法，并且比较std::unordered_map的两种遍历方式。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/15/bpe-trainer-6/&quot;&gt;第6部分：用OpenMP实现并行求最大&lt;/a&gt; 用OpenMP并行求pair_counts里最大pair。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/18/bpe-trainer-7/&quot;&gt;第7部分：使用flat hashmap替代std::unordered_map&lt;/a&gt; 使用flat hashmap来替代std::unordered_map。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/19/bpe-trainer-8/&quot;&gt;第8部分：实现细粒度更新&lt;/a&gt; 使用倒排索引实现pair_counts的细粒度更新算法。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/21/bpe-trainer-9/&quot;&gt;第9部分：使用堆来寻找最大pair&lt;/a&gt; 使用堆来求最大pair，提升性能。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/24/bpe-trainer-10/&quot;&gt;第10部分：使用cython和pypy来加速&lt;/a&gt; 使用cython和pypy来加速python代码。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/25/bpe-trainer-11/&quot;&gt;第11部分：使用cython封装c++代码&lt;/a&gt; 使用cython封装c++代码。&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 25 Sep 2025 00:00:00 +0000</pubDate>
        <link>http://fancyerii.github.io/2025/09/25/bpe-trainer-11/</link>
        <guid isPermaLink="true">http://fancyerii.github.io/2025/09/25/bpe-trainer-11/</guid>
        
        <category>cs336</category>
        
        <category>bpe tokenizer</category>
        
        
      </item>
    
      <item>
        <title>Implementing and Optimizing a BPE Tokenizer from Scratch—Part 10: Using Cython and PyPy for Acceleration</title>
        <description>&lt;p&gt;This series of articles implements a subtask of Stanford’s CS336 Assignment 1: building an efficient training algorithm for a BPE Tokenizer. Through a series of optimizations, our algorithm’s training time on OpenWebText was reduced from over 10 hours to less than 10 minutes. This series explains these optimizations, including algorithmic improvements, data structure enhancements, parallelization with OpenMP, Cython optimization, and implementing key code in C++ along with its integration via Cython. This article, the eleventh in the series, will cover using Cython and PyPy to accelerate Python code.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;strong&gt;Table of Content&lt;/strong&gt;&lt;/p&gt;
&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#1-problem-analysis&quot; id=&quot;markdown-toc-1-problem-analysis&quot;&gt;1. Problem Analysis&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-introduction-to-cython-principles&quot; id=&quot;markdown-toc-2-introduction-to-cython-principles&quot;&gt;2. Introduction to Cython Principles&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-optimizing-the-fine_grained_pair_counter_diff-function-with-cython&quot; id=&quot;markdown-toc-3-optimizing-the-fine_grained_pair_counter_diff-function-with-cython&quot;&gt;3. Optimizing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff&lt;/code&gt; function with Cython&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-test-results&quot; id=&quot;markdown-toc-4-test-results&quot;&gt;4. Test Results&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-implementing-the-entire-merge-process-with-cython&quot; id=&quot;markdown-toc-5-implementing-the-entire-merge-process-with-cython&quot;&gt;5. Implementing the entire merge process with Cython&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-using-pypy&quot; id=&quot;markdown-toc-6-using-pypy&quot;&gt;6. Using PyPy&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#7-conclusion&quot; id=&quot;markdown-toc-7-conclusion&quot;&gt;7. Conclusion&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#full-series&quot; id=&quot;markdown-toc-full-series&quot;&gt;Full Series&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;1-problem-analysis&quot;&gt;1. Problem Analysis&lt;/h2&gt;

&lt;p&gt;In the previous article, we have already performed extreme optimizations on the algorithm. The fastest Python version, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7_maxheapc_opt_time&lt;/code&gt;, has a merge time of over 500 seconds, while the fastest C++ version, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt&lt;/code&gt;, has a merge time of 100 seconds. Later, we will integrate the C++ code with the previous Python code using Cython. However, today, we want to perform some optimizations at the Python level, as using two languages is quite troublesome.&lt;/p&gt;

&lt;p&gt;For the same logic, Python is slower than C++ for many reasons. The main reasons are: Python (more accurately, CPython) interprets code while C++ compiles to machine code; Python is dynamically typed while C++ is statically typed; Python’s GIL prevents true multithreading parallelism; and Python’s memory layout is not CPU-friendly.&lt;/p&gt;

&lt;p&gt;The third reason was analyzed when comparing Python’s multiprocessing and C++/OpenMP parallel max search. Furthermore, through algorithmic optimization, we no longer need to scan the entire &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; to find the max, so we can ignore this difference.&lt;/p&gt;

&lt;p&gt;The fourth reason is also very important. Even in C++, if you use a linked-list memory layout like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt;, its traversal speed is much slower than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashMap&lt;/code&gt;. In Python, everything is an object, which from a memory layout perspective is essentially a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;void *&lt;/code&gt;. Therefore, although Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; looks similar to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::vector&lt;/code&gt;, their memory layouts are completely different.&lt;/p&gt;

&lt;p&gt;When we create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict&lt;/code&gt; with integer keys and values, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{1: 10, 2: 20}&lt;/code&gt;, Python actually does the following in memory:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Creates a hash table structure for the dictionary itself.&lt;/li&gt;
  &lt;li&gt;Creates a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PyLongObject&lt;/code&gt; for the key &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Creates a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PyLongObject&lt;/code&gt; for the value &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Stores the references (memory addresses) of the key and value objects in a hash table entry.&lt;/li&gt;
  &lt;li&gt;Repeats the process for key &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; and value &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;20&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means that even if your keys and values are just small integers, each integer comes with the overhead of a full Python object (including type information, reference counting, etc.). This design makes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict&lt;/code&gt; extremely flexible, allowing it to store data of any type, but at the cost of consuming more memory.&lt;/p&gt;

&lt;p&gt;However, at the Python level, it’s difficult to optimize &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict&lt;/code&gt;. This is the cost of using Python. If you want to optimize memory, the best way is to use other languages (like C/C++) and integrate them via the Python/C API. Our previous max-heap algorithm was integrated via the Python/C API, but this API is very complex. We will later use Cython to integrate the C++ code, as Cython ultimately compiles our C++ code into an extension module. Similarly, NumPy is integrated via the Python/C API.&lt;/p&gt;

&lt;p&gt;The remaining two points are the focus of today’s optimization: first, we’ll try to rewrite some key code with Cython; second, we’ll try to use PyPy to replace CPython.&lt;/p&gt;

&lt;h2 id=&quot;2-introduction-to-cython-principles&quot;&gt;2. Introduction to Cython Principles&lt;/h2&gt;

&lt;p&gt;Cython is a programming language that combines the ease of use of Python with the high performance of C. It is designed to be a &lt;strong&gt;superset of Python&lt;/strong&gt;, which means you can use it to write regular Python code while also adding C language features, such as static type declarations. The core idea of Cython is to compile your Python code into C language, and then compile it into machine code to form an importable Python module. This process solves the performance bottleneck of the Python interpreter.&lt;/p&gt;

&lt;p&gt;For a detailed introduction to Cython, please refer to the &lt;a href=&quot;https://cython.readthedocs.io/en/stable/index.html&quot;&gt;official documentation&lt;/a&gt;. Additionally, if readers want to learn Cython systematically, they can read the book &lt;a href=&quot;https://www.amazon.com/Cython-Programmers-Kurt-W-Smith/dp/1491901551&quot;&gt;Cython: A Guide for Python Programmers&lt;/a&gt;. Although it’s an old book, most of its content is still useful.&lt;/p&gt;

&lt;h2 id=&quot;3-optimizing-the-fine_grained_pair_counter_diff-function-with-cython&quot;&gt;3. Optimizing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff&lt;/code&gt; function with Cython&lt;/h2&gt;

&lt;p&gt;Our optimization is based on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7_opt&lt;/code&gt;. According to our previous analysis, the time for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max&lt;/code&gt; can be ignored, and most of the remaining time is spent in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_updated_affected_word_count&lt;/code&gt; function. This function mainly calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff&lt;/code&gt; and then updates the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_to_words&lt;/code&gt; dictionaries. Updating dictionaries is difficult to optimize with Cython, so we will first optimize the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;We first create a &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_updater.pyx&quot;&gt;bpe_updater.pyx&lt;/a&gt; file, which implements &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff&lt;/code&gt; with Cython.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef void fine_grained_pair_counter_diff(set affected_words, 
                                          word_encodings, 
                                          word_counts, 
                                          tuple merge_pair, 
                                          diff_pairs, 
                                          int new_id, 
                                          pair_to_words, 
                                          set new_pairs):
    cdef str word
    cdef int wc
    cdef int idx
    cdef int first_idx
    cdef int last_idx
    cdef int i
    cdef int tk_len

    for word in affected_words:
        word_tokens = word_encodings[word]
        wc = word_counts[word]

        # find first and last pairs
        idx = 0
        unaffected_pairs = set()
        tk_len = len(word_tokens)
        #first_idx = -1
        while idx &amp;lt; tk_len - 1:
            if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                first_idx = idx
                break
            idx += 1

        # assert first_idx exists

        idx = tk_len - 2
        while idx &amp;gt; first_idx + 1:
            if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                last_idx = idx
                break
            idx -= 1
        else:
            last_idx = first_idx

        start_idx = max_int(0, first_idx - 1) # inclusive
        end_idx = min_int(last_idx + 3, tk_len) # exclusive

        # unaffected [0, start_idx)


        for i in range(start_idx):
            pair = word_tokens[i], word_tokens[i + 1]
            unaffected_pairs.add(pair)
        # unaffected [end_idx-1, :-1]
        for i in range(end_idx - 1, tk_len - 1):
            pair = word_tokens[i], word_tokens[i + 1]
            unaffected_pairs.add(pair)                

        # TODO avoid slice copy
        affected_tokens = word_tokens[start_idx: end_idx]
        for i in range(len(affected_tokens) - 1):
            old_pair = (affected_tokens[i], affected_tokens[i + 1])
            diff_pairs[old_pair] -= wc 
            if old_pair not in unaffected_pairs:   
                pair_to_words[old_pair].discard(word)
        

        new_tokens = []
        all_new_tokens = []
        for i in range(start_idx):
            all_new_tokens.append(word_tokens[i])
        
        i = 0
        # account for multiple occurrences of the pair
        while i &amp;lt; len(affected_tokens):
            if i &amp;lt; len(affected_tokens) - 1 and (affected_tokens[i], affected_tokens[i + 1]) == merge_pair:
                new_tokens.append(new_id)
                all_new_tokens.append(new_id)
                # jump past pair
                i += 2
            else:
                new_tokens.append(affected_tokens[i])
                all_new_tokens.append(affected_tokens[i])
                i += 1



        for i in range(end_idx, len(word_tokens)):
            all_new_tokens.append(word_tokens[i])
        
        word_encodings[word] = all_new_tokens

        # add new pairs from the updated word
        for i in range(len(new_tokens) - 1):
            new_pair = (new_tokens[i], new_tokens[i + 1])

            diff_pairs[new_pair] += wc
            pair_to_words[new_pair].add(word)

            new_pairs.add(new_pair)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This code is essentially the same as the Python version, except that static type declarations have been added for the loop variables. This allows Cython to compile the loops into C-language versions, bypassing the Python iterator protocol.&lt;/p&gt;

&lt;p&gt;Additionally, we can see in the code above:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;affected_tokens = word_tokens[start_idx: end_idx]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This slice will copy the affected tokens. Our C++ implementation &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained.cpp%23L216&quot;&gt;code&lt;/a&gt; avoids this copy by using pointers:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;        const int * affected_tokens = word_tokens.data() + start_idx;
        int affected_tokens_len = end_idx - start_idx;

        for(int i = 0; i &amp;lt; affected_tokens_len - 1; ++i){
            std::pair&amp;lt;int, int&amp;gt; old_pair(affected_tokens[i], affected_tokens[i + 1]);
            diff_pairs[old_pair] -= wc;
            if (unaffected_pairs.find(old_pair) == unaffected_pairs.end()) {
                pair_wordids[old_pair].erase(wordid);
            }
        }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; cannot return a view, so we must manually handle the tedious indices to avoid copying. This gives us &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff_v2&lt;/code&gt;. We won’t go into the detailed changes here, but interested readers can refer to &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_updater.pyx#L9&quot;&gt;fine_grained_pair_counter_diff_v2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Furthermore, based on the optimization from the previous article, we can remove &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new_pairs&lt;/code&gt;, which leads to &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_updater.pyx#L205&quot;&gt;fine_grained_pair_counter_diff_v3&lt;/a&gt; based on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff_v2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, we need to modify &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setup.py&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;setup(
    packages=[&apos;cs336_basics&apos;],
    name=&apos;cs336_basics.bpe_updater&apos;,
    ext_modules=cythonize(&quot;cs336_basics/bpe_updater.pyx&quot;),
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then execute &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python setup.py build_ext -i&lt;/code&gt;, which compiles to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cs336_basics/bpe_updater.cpython-312-x86_64-linux-gnu.so&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, we use this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_updater&lt;/code&gt; extension module in our Python code, with the corresponding code in &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v8.py&quot;&gt;bpe_v8.py&lt;/a&gt;, &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v8_v2.py&quot;&gt;bpe_v8_v2.py&lt;/a&gt;, and &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v8_v3.py&quot;&gt;bpe_v8_v3.py&lt;/a&gt;. They call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff_v2&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff_v3&lt;/code&gt;, respectively.&lt;/p&gt;

&lt;h2 id=&quot;4-test-results&quot;&gt;4. Test Results&lt;/h2&gt;

&lt;p&gt;Since gathering statistics for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; introduces extra time overhead (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7_time&lt;/code&gt; is over 10 seconds slower than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7&lt;/code&gt;), I will only report the total merge time here.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Version&lt;/th&gt;
      &lt;th&gt;Data&lt;/th&gt;
      &lt;th&gt;Total Time (s)&lt;/th&gt;
      &lt;th&gt;Word Freq Time (s)&lt;/th&gt;
      &lt;th&gt;Merge Time (s)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1062/1035/1036&lt;/td&gt;
      &lt;td&gt;392/397/395&lt;/td&gt;
      &lt;td&gt;total: 606/573/577&lt;br /&gt;max:6/6/6&lt;br /&gt;update: 599/567/571&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1051/1017/1023&lt;/td&gt;
      &lt;td&gt;399/389/398&lt;/td&gt;
      &lt;td&gt;590/568/568&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_opt&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1021/1007/980&lt;/td&gt;
      &lt;td&gt;399/393/393&lt;/td&gt;
      &lt;td&gt;558/553/528&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;934/930/935&lt;/td&gt;
      &lt;td&gt;390/393/393&lt;/td&gt;
      &lt;td&gt;479/472/476&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8_v2&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;917/908/965&lt;/td&gt;
      &lt;td&gt;394/392/395&lt;/td&gt;
      &lt;td&gt;460/455/505&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8_v3&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;897/899/951&lt;/td&gt;
      &lt;td&gt;395/399/395&lt;/td&gt;
      &lt;td&gt;442/438/493&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The implementation logic of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v8&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7&lt;/code&gt; is identical. We can see that the Cython version &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v8&lt;/code&gt; has a merge time that is 17% faster than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v8_v2&lt;/code&gt; avoids copying &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;affected_tokens = word_tokens[start_idx: end_idx]&lt;/code&gt;, and its first two runs are over 10 seconds faster than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v8&lt;/code&gt;, but the third run is 30 seconds slower. The reason for this is unclear, perhaps due to fluctuations in server load. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7_opt&lt;/code&gt; made the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new_pairs&lt;/code&gt; deletion optimization over &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7&lt;/code&gt;, and it is compared with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v8_v3&lt;/code&gt;, which also includes the copy-avoidance optimization from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v8_v2&lt;/code&gt;. The average time for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v8_v3&lt;/code&gt; is 457 seconds, which is 16% faster than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7_opt&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;5-implementing-the-entire-merge-process-with-cython&quot;&gt;5. Implementing the entire merge process with Cython&lt;/h2&gt;

&lt;p&gt;To go a step further, can implementing the entire merge process with Cython make it even faster? By refactoring &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v8_v3&lt;/code&gt;, I implemented &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_updater_v2.pyx&quot;&gt;bpe_updater_v2.pyx&lt;/a&gt; and &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v8_v4.py&quot;&gt;bpe_v8_v4.py&lt;/a&gt;. Here, I encapsulated the merge process into a single function:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef void bpe_train_step2(int vocab_size,
                      pair_counts,
                      pair_strings,
                      vocabulary,
                      pair_to_words,
                      word_counts,
                      word_encodings,
                      merges,
                      pair_heap):

    cdef int size = len(vocabulary)
    while size &amp;lt; vocab_size:
        _merge_a_pair(pair_counts, pair_strings, vocabulary,
                                pair_to_words, word_counts, word_encodings,
                                merges, size, pair_heap)
        size += 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The test results are as follows:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Version&lt;/th&gt;
      &lt;th&gt;Data&lt;/th&gt;
      &lt;th&gt;Total Time (s)&lt;/th&gt;
      &lt;th&gt;Word Freq Time (s)&lt;/th&gt;
      &lt;th&gt;Merge Time (s)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1062/1035/1036&lt;/td&gt;
      &lt;td&gt;392/397/395&lt;/td&gt;
      &lt;td&gt;total: 606/573/577&lt;br /&gt;max:6/6/6&lt;br /&gt;update: 599/567/571&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1051/1017/1023&lt;/td&gt;
      &lt;td&gt;399/389/398&lt;/td&gt;
      &lt;td&gt;590/568/568&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_opt&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1021/1007/980&lt;/td&gt;
      &lt;td&gt;399/393/393&lt;/td&gt;
      &lt;td&gt;558/553/528&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;934/930/935&lt;/td&gt;
      &lt;td&gt;390/393/393&lt;/td&gt;
      &lt;td&gt;479/472/476&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8_v2&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;917/908/965&lt;/td&gt;
      &lt;td&gt;394/392/395&lt;/td&gt;
      &lt;td&gt;460/455/505&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8_v3&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;897/899/951&lt;/td&gt;
      &lt;td&gt;395/399/395&lt;/td&gt;
      &lt;td&gt;442/438/493&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8_v4&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;917/915/982&lt;/td&gt;
      &lt;td&gt;391/404/400&lt;/td&gt;
      &lt;td&gt;462/448/506&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v8_v4&lt;/code&gt; is even slower than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v8_v3&lt;/code&gt;. The reason is that in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff&lt;/code&gt; function, we used Cython’s static type declarations to turn the Python iterator loop into a C loop, making it faster. However, the rest of the code operates on Python dictionaries. Implementing this code in Cython still reverts to Python, which is slower.&lt;/p&gt;

&lt;h2 id=&quot;6-using-pypy&quot;&gt;6. Using PyPy&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://pypy.org/&quot;&gt;PyPy&lt;/a&gt; is an alternative implementation of the Python language. Simply put, it’s not a library or a framework, but a complete Python interpreter, just like the CPython we commonly use. PyPy’s biggest highlight is its built-in &lt;strong&gt;JIT (Just-In-Time) compiler&lt;/strong&gt;. This is the biggest difference from the standard CPython interpreter and the reason for its speed. CPython compiles Python code into bytecode, which is then executed one line at a time by a virtual machine. This process is relatively slow. PyPy also first compiles Python code into bytecode. However, while the program is running, PyPy’s JIT compiler monitors which code is frequently executed. It then compiles this “hotspot” code directly into machine code and caches it. The next time it encounters the same code, PyPy executes the high-speed machine code directly instead of re-interpreting the bytecode. PyPy is quite similar to Java’s JVM.&lt;/p&gt;

&lt;p&gt;To switch the environment from CPython to PyPy, you don’t need to modify any code, but you do need to reinstall the environment. Since we are using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; to manage our environment, switching is very simple; just run the following commands:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;deactivate
UV_PROJECT_ENVIRONMENT=.venv_pypy uv sync --python pypy@3.11
source .venv_pypy/bin/activate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The first command exits the current virtual environment. Then, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv sync&lt;/code&gt; is used to create a new PyPy 3.11 environment. To avoid overwriting the original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.venv&lt;/code&gt;, we use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UV_PROJECT_ENVIRONMENT&lt;/code&gt; environment variable to tell &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; to create the new virtual environment in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.venv_pypy&lt;/code&gt; directory. Finally, we &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source&lt;/code&gt; this environment.&lt;/p&gt;

&lt;p&gt;I first tested &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7&lt;/code&gt;, and the results are as follows:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Version&lt;/th&gt;
      &lt;th&gt;Data&lt;/th&gt;
      &lt;th&gt;Total Time (s)&lt;/th&gt;
      &lt;th&gt;Word Freq Time (s)&lt;/th&gt;
      &lt;th&gt;Merge Time (s)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1028/1053/1045&lt;/td&gt;
      &lt;td&gt;395/397/393&lt;/td&gt;
      &lt;td&gt;total: 575/589/590&lt;br /&gt;max: 6/6/6&lt;br /&gt;update: 569/583/583&lt;br /&gt;make heap: 0.01/0.01&lt;br /&gt;heap_push_time: 102/107/122&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7(pypy)&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;2047/1694/1913&lt;/td&gt;
      &lt;td&gt;1644/1306/1552&lt;/td&gt;
      &lt;td&gt;403/388/361&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The results show that PyPy’s overall runtime is much slower than CPython, increasing from over 1000 seconds to over 2000 seconds. However, PyPy’s merge time is 34% faster than CPython. Why is there a speed discrepancy? Through a series of tests, I pinpointed the issue to the speed difference in regex matching, and then I wrote a program, &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v2_time_pypy.py&quot;&gt;bpe_v2_time_pypy.py&lt;/a&gt;. This program only executes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_pretokenize_and_count&lt;/code&gt; function and measures its time. The following are the results from testing on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tinystory&lt;/code&gt; dataset with a single CPU (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_counter=1&lt;/code&gt;) under PyPy 3.11, CPython 3.12, and CPython 3.11:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Version&lt;/th&gt;
      &lt;th&gt;Data&lt;/th&gt;
      &lt;th&gt;split_time (s)&lt;/th&gt;
      &lt;th&gt;match_time (s)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v2_pypy(pypy 3.11)&lt;/td&gt;
      &lt;td&gt;tiny_story&lt;/td&gt;
      &lt;td&gt;40&lt;/td&gt;
      &lt;td&gt;888&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v2_pypy(cpython 3.12)&lt;/td&gt;
      &lt;td&gt;tiny_story&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;300&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v2_pypy(cpython 3.11)&lt;/td&gt;
      &lt;td&gt;tiny_story&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;288&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;split_time&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;match_time&lt;/code&gt; respectively measure the time for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;regex.split&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;regex.finditer&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;        for chunk in BPE_Trainer._chunk_documents_streaming(input_path):
            start_time = time.perf_counter()
            blocks = re.split(special_pattern, chunk)
            end_time = time.perf_counter()
            split_time += (end_time - start_time)
            for block in blocks:
                start_time = time.perf_counter()
                for match in re.finditer(pattern, block):
                    text = match.group(0)
                    text_len += len(text)
                end_time = time.perf_counter()
                match_time += (end_time - start_time)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As we can see, the PyPy version is much slower than CPython. To find the reason, I submitted an issue on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;regex&lt;/code&gt; GitHub repository: &lt;a href=&quot;https://github.com/mrabarnett/mrab-regex/issues/586&quot;&gt;regex is much slower in pypy than cpython&lt;/a&gt;. According to mattip’s reply, the reason for PyPy’s slowness is that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;regex&lt;/code&gt; uses CPython’s C API, and PyPy can only implement this API through emulation, which is very slow. Not only is it slow, but the results can even be incorrect. According to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;regex&lt;/code&gt; documentation:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;This module is targeted at CPython. It expects that all codepoints are the same width, so it won’t behave properly with pypy outside U+0000..U+007F because pypy stores strings as UTF-8.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s very unfortunate that while PyPy’s JIT compilation does speed up the subsequent merge process, the unsupported &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;regex&lt;/code&gt; library makes our overall PyPy run slower and even unusable. Of course, we could split the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;train&lt;/code&gt; function into two parts: the first part, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_pretokenize_and_count&lt;/code&gt;, uses CPython, and the second part, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge&lt;/code&gt;, uses PyPy, with inter-process communication between them. However, inter-process communication is complex and data copying also introduces significant overhead.&lt;/p&gt;

&lt;p&gt;Therefore, the biggest problem with PyPy is its lack of “compatibility” with CPython, while in reality, many high-performance third-party libraries use CPython’s C API. For example, NumPy: PyPy cannot directly accelerate NumPy’s underlying C code. Instead, it communicates with NumPy’s C extension through a compatibility layer called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cpyext&lt;/code&gt;. This compatibility layer is designed to allow PyPy to run CPython’s C extensions. This means that when only using NumPy, PyPy can be even slower than CPython.&lt;/p&gt;

&lt;h2 id=&quot;7-conclusion&quot;&gt;7. Conclusion&lt;/h2&gt;

&lt;p&gt;Although PyPy can accelerate Python code execution, we have to give it up because of the lack of support from the third-party library &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;regex&lt;/code&gt;. On the other hand, with Cython, we were able to achieve the faster speed without modifying the Python code much (though some code was refactored for module calls). Of course, we can further optimize the Cython code, for example, by using C++’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unordered_map&lt;/code&gt; instead of Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict&lt;/code&gt;. However, a better approach is to perform these optimizations directly in C++ and then package them into a dynamic library for Cython to call. This will be the topic of our next article.&lt;/p&gt;

&lt;h2 id=&quot;full-series&quot;&gt;Full Series&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/05/bpe-trainer-0_en/&quot;&gt;Part 0: Introduction&lt;/a&gt; Introduces the basic BPE training algorithm and related tasks, as well as the development environment.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/07/bpe-trainer-1_en/&quot;&gt;Part 1: The Simplest Implementation&lt;/a&gt; The simplest implementation of BPE training.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/08/bpe-trainer-2_en/&quot;&gt;Part 2: Optimized Algorithm&lt;/a&gt; Implements incremental updates for pair_counts.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/09/bpe-trainer-3_en/&quot;&gt;Part 3: Parallel Tokenization and Frequency Counting&lt;/a&gt; Uses multiprocessing to implement a multi-process parallel algorithm.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/10/bpe-trainer-4_en/&quot;&gt;Part 4: A Failed Parallel Optimization&lt;/a&gt; An attempt to parallelize the max pair calculation using multiple processes.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/12/bpe-trainer-5_en/&quot;&gt;Part 5: Implementing the Merge Algorithm in C++&lt;/a&gt; Implements a C++ merge algorithm equivalent to the Python version, and compares two ways of iterating through std::unordered_map.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/15/bpe-trainer-6_en/&quot;&gt;Part 6: Parallelizing the Max Pair Search with OpenMP&lt;/a&gt; Uses OpenMP to find the max pair in pair_counts in parallel.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/18/bpe-trainer-7_en/&quot;&gt;Part 7: Using Flat Hashmap to Replace std::unordered_map&lt;/a&gt; Uses flat hashmap to replace std::unordered_map.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/19/bpe-trainer-8_en/&quot;&gt;Part 8: Implementing Fine-Grained Updates&lt;/a&gt; Implements a fine-grained update algorithm for pair_counts using an inverted index.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/21/bpe-trainer-9_en/&quot;&gt;Part 9: Using a Heap to Find the Max Pair&lt;/a&gt; Uses a heap to find the max pair and improve performance.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/24/bpe-trainer-10_en/&quot;&gt;Part 10: Using Cython and PyPy for Acceleration&lt;/a&gt; Uses Cython and PyPy to accelerate Python code.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/25/bpe-trainer-11_en/&quot;&gt;Part 11: Wrapping C++ Code with Cython&lt;/a&gt; Wraps C++ code using Cython.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 24 Sep 2025 00:00:00 +0000</pubDate>
        <link>http://fancyerii.github.io/2025/09/24/bpe-trainer-10_en/</link>
        <guid isPermaLink="true">http://fancyerii.github.io/2025/09/24/bpe-trainer-10_en/</guid>
        
        <category>cs336</category>
        
        <category>bpe tokenizer</category>
        
        
      </item>
    
      <item>
        <title>动手实现和优化BPE Tokenizer的训练——第10部分：使用cython和pypy来加速</title>
        <description>&lt;p&gt;本系列文章完成Stanford CS336作业1的一个子任务——实现BPE Tokenizer的高效训练算法。通过一系列优化，我们的算法在OpenWebText上的训练时间从最初的10多个小时优化到小于10分钟。本系列文章解释这一系列优化过程，包括：算法的优化，数据结构的优化，并行(openmp)优化，cython优化，用c++实现关键代码和c++库的cython集成等内容。本文是第十一篇，使用cython和pypy来加速python代码。&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#1-问题分析&quot; id=&quot;markdown-toc-1-问题分析&quot;&gt;1. 问题分析&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-cython原理简介&quot; id=&quot;markdown-toc-2-cython原理简介&quot;&gt;2. cython原理简介&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-用cython优化fine_grained_pair_counter_diff函数&quot; id=&quot;markdown-toc-3-用cython优化fine_grained_pair_counter_diff函数&quot;&gt;3. 用cython优化fine_grained_pair_counter_diff函数&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-测试结果&quot; id=&quot;markdown-toc-4-测试结果&quot;&gt;4. 测试结果&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-把整个merge过程用cython实现&quot; id=&quot;markdown-toc-5-把整个merge过程用cython实现&quot;&gt;5. 把整个merge过程用cython实现&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-使用pypy&quot; id=&quot;markdown-toc-6-使用pypy&quot;&gt;6. 使用pypy&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#7-总结&quot; id=&quot;markdown-toc-7-总结&quot;&gt;7. 总结&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#本系列全部文章&quot; id=&quot;markdown-toc-本系列全部文章&quot;&gt;本系列全部文章&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;1-问题分析&quot;&gt;1. 问题分析&lt;/h2&gt;

&lt;p&gt;上一篇文章我们已经对算法做了极致的优化，目前Python版本最快的是bpe_v7_maxheapc_opt_time，它的merge时间是500多秒；C++版本最快的是bpe_train_updater_fine_grained_heap_emhash8_set9_opt，它的merge时间是100秒。后面我们会把c++版本的代码通过cython和前面的python代码集成。不过今天我们想要在python的层面做一些优化，毕竟使用两种语言还是比较麻烦。&lt;/p&gt;

&lt;p&gt;同样的代码逻辑，python比c++要慢，这有很多的原因。最主要的原因有：python(更准确的说是CPython)是通过解释执行代码而c++是编译成机器码执行；python是动态类型而c++是静态类型；python的GIL锁使得python多线程无法真正并行；python的内存布局对于cpu不友好。&lt;/p&gt;

&lt;p&gt;第三点原因在前面比较python的多进程和c++/openmp并行求max已经分析过了，而且通过算法的优化，我们不需要扫描整个pair_counts来求max，所以这个差异我们可以忽略。&lt;/p&gt;

&lt;p&gt;第四点其实也是很重要的，即使是c++，如果使用std::unordered_map这样的链表这种内存布局，它的遍历速度也比absl::flat_hash_map和emhash8::HashMap要慢很多。在python里，所有的一切都是对象，其实从内存布局来说就是void *，所以python的list的用法虽然看起来和std::vector很像，但是它们内存布局完全不同。&lt;/p&gt;

&lt;p&gt;当我们创建一个key/value是整数的dict时，比如 {1: 10, 2: 20}，Python 在内存中实际做了以下几件事：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;为字典本身创建一个哈希表结构。&lt;/li&gt;
  &lt;li&gt;为键 1 创建一个 PyLongObject。&lt;/li&gt;
  &lt;li&gt;为值 10 创建一个 PyLongObject。&lt;/li&gt;
  &lt;li&gt;将键对象和值对象的引用（内存地址）存储到哈希表中的一个条目里。&lt;/li&gt;
  &lt;li&gt;对键 2 和值 20 重复上述过程。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这意味着，即使你的键和值只是一些小小的整数，每个整数背后都有一个完整的 Python 对象开销（包括类型信息、引用计数等）。这种设计使得 dict 极为灵活，可以存储任何类型的数据，但代价就是会消耗更多的内存。&lt;/p&gt;

&lt;p&gt;不过在python层面，我们很难对dict进行优化。这是使用python代价，如果想要优化内存，最好的方法是使用其它的语言(比如c/c++)然后通过Python/C API集成。我们之前的大堆算法就是通过Python/C API集成，不过这个API非常复杂。我们后面会通过cython把c++的代码集成，cython最终也是把我们的c++代码编译成扩展模块。类似的numpy就是通过Python/C集成。&lt;/p&gt;

&lt;p&gt;剩下的第一点和第二点就是我们今天优化的主题：一是尝试通过cython来重写部分关键代码；二是尝试使用pypy来替代CPython。&lt;/p&gt;

&lt;h2 id=&quot;2-cython原理简介&quot;&gt;2. cython原理简介&lt;/h2&gt;

&lt;p&gt;Cython是一种编程语言，它结合了Python的易用性和C语言的高性能。它旨在成为Python的超集，这意味着你可以用它来编写普通的Python代码，同时也能添加额外的C语言特性，比如静态类型声明。Cython的核心思想是将你的代码从Python语言编译成C语言，然后再编译成机器码，形成一个可导入的Python模块。这个过程解决了Python解释器在执行速度上的瓶颈。&lt;/p&gt;

&lt;p&gt;关于cython的详细介绍请参考&lt;a href=&quot;https://cython.readthedocs.io/en/stable/index.html&quot;&gt;官方文档&lt;/a&gt;，另外如果读者想系统学习cython也可以阅读书籍&lt;a href=&quot;https://www.amazon.com/Cython-Programmers-Kurt-W-Smith/dp/1491901551&quot;&gt;Cython: A Guide for Python Programmers&lt;/a&gt;，这本书虽然很久远了，但是大部分内容依然又用。&lt;/p&gt;

&lt;h2 id=&quot;3-用cython优化fine_grained_pair_counter_diff函数&quot;&gt;3. 用cython优化fine_grained_pair_counter_diff函数&lt;/h2&gt;

&lt;p&gt;我们的优化基于bpe_v7_opt，根据之前的分析，max的时间已经可以忽略，剩下大部分时间在函数_updated_affected_word_count。这个函数主要是调用fine_grained_pair_counter_diff然后更新pair_counts和pair_to_words这两个词典。更新词典很难用cython优化，所以我们先优化fine_grained_pair_counter_diff函数。&lt;/p&gt;

&lt;p&gt;我们首先创建一个&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_updater.pyx&quot;&gt;bpe_updater.pyx&lt;/a&gt;文件，它用cython实现了fine_grained_pair_counter_diff。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef void fine_grained_pair_counter_diff(set affected_words, 
                                          word_encodings, 
                                          word_counts, 
                                          tuple merge_pair, 
                                          diff_pairs, 
                                          int new_id, 
                                          pair_to_words, 
                                          set new_pairs):
    cdef str word
    cdef int wc
    cdef int idx
    cdef int first_idx
    cdef int last_idx
    cdef int i
    cdef int tk_len

    for word in affected_words:
        word_tokens = word_encodings[word]
        wc = word_counts[word]

        # find first and last pairs
        idx = 0
        unaffected_pairs = set()
        tk_len = len(word_tokens)
        #first_idx = -1
        while idx &amp;lt; tk_len - 1:
            if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                first_idx = idx
                break
            idx += 1

        # assert first_idx exists

        idx = tk_len - 2
        while idx &amp;gt; first_idx + 1:
            if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                last_idx = idx
                break
            idx -= 1
        else:
            last_idx = first_idx

        start_idx = max_int(0, first_idx - 1) # inclusive
        end_idx = min_int(last_idx + 3, tk_len) # exclusive

        # unaffected [0, start_idx)


        for i in range(start_idx):
            pair = word_tokens[i], word_tokens[i + 1]
            unaffected_pairs.add(pair)
        # unaffected [end_idx-1, :-1]
        for i in range(end_idx - 1, tk_len - 1):
            pair = word_tokens[i], word_tokens[i + 1]
            unaffected_pairs.add(pair)                

        # TODO avoid slice copy
        affected_tokens = word_tokens[start_idx: end_idx]
        for i in range(len(affected_tokens) - 1):
            old_pair = (affected_tokens[i], affected_tokens[i + 1])
            diff_pairs[old_pair] -= wc 
            if old_pair not in unaffected_pairs:   
                pair_to_words[old_pair].discard(word)
        

        new_tokens = []
        all_new_tokens = []
        for i in range(start_idx):
            all_new_tokens.append(word_tokens[i])
        
        i = 0
        # account for multiple occurrences of the pair
        while i &amp;lt; len(affected_tokens):
            if i &amp;lt; len(affected_tokens) - 1 and (affected_tokens[i], affected_tokens[i + 1]) == merge_pair:
                new_tokens.append(new_id)
                all_new_tokens.append(new_id)
                # jump past pair
                i += 2
            else:
                new_tokens.append(affected_tokens[i])
                all_new_tokens.append(affected_tokens[i])
                i += 1



        for i in range(end_idx, len(word_tokens)):
            all_new_tokens.append(word_tokens[i])
        
        word_encodings[word] = all_new_tokens

        # add new pairs from the updated word
        for i in range(len(new_tokens) - 1):
            new_pair = (new_tokens[i], new_tokens[i + 1])

            diff_pairs[new_pair] += wc
            pair_to_words[new_pair].add(word)

            new_pairs.add(new_pair)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个代码和python的版本基本一样，只不过把循环变量都加了静态类型声明，这样cython就可以把它编译成c语言版本的for/while循环，而不需要走python的迭代器协议。&lt;/p&gt;

&lt;p&gt;另外我们可以发现上面的代码：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;affected_tokens = word_tokens[start_idx: end_idx]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个slice会复制受影响的tokens，我们在c++实现的&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained.cpp#L216&quot;&gt;代码&lt;/a&gt;使用了指针避免复制：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;        const int * affected_tokens = word_tokens.data() + start_idx;
        int affected_tokens_len = end_idx - start_idx;

        for(int i = 0; i &amp;lt; affected_tokens_len - 1; ++i){
            std::pair&amp;lt;int, int&amp;gt; old_pair(affected_tokens[i], affected_tokens[i + 1]);
            diff_pairs[old_pair] -= wc;
            if (unaffected_pairs.find(old_pair) == unaffected_pairs.end()) {
                pair_wordids[old_pair].erase(wordid);
            }
        }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;python的list没有办法返回一个视图，所以我们只能自己处理繁琐的下标来避免复制，这样我们得到fine_grained_pair_counter_diff_v2。这里就不详细介绍其中的改动了，感兴趣的读者可以阅读&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_updater.pyx#L9&quot;&gt;fine_grained_pair_counter_diff_v2&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;此外根据上一篇文章的优化，我们可以把new_pairs去掉，这样在fine_grained_pair_counter_diff_v2的基础上得到&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_updater.pyx#L205&quot;&gt;fine_grained_pair_counter_diff_v3&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;然后我们需要修改setup.py：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;setup(
    packages=[&apos;cs336_basics&apos;],
    name=&apos;cs336_basics.bpe_updater&apos;,
    ext_modules=cythonize(&quot;cs336_basics/bpe_updater.pyx&quot;),
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后执行python setup.py build_ext -i，这样编译得到cs336_basics/bpe_updater.cpython-312-x86_64-linux-gnu.so。&lt;/p&gt;

&lt;p&gt;最后我们在python里使用bpe_updater这个扩展模块，相应的代码在&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v8.py&quot;&gt;bpe_v8.py&lt;/a&gt;、&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v8_v2.py&quot;&gt;bpe_v8_v2.py&lt;/a&gt;和&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v8_v3.py&quot;&gt;bpe_v8_v3.py&lt;/a&gt;。它们分别调用fine_grained_pair_counter_diff、fine_grained_pair_counter_diff_v2和fine_grained_pair_counter_diff_v3。&lt;/p&gt;

&lt;h2 id=&quot;4-测试结果&quot;&gt;4. 测试结果&lt;/h2&gt;

&lt;p&gt;因为统计max和update会带来额外的时间开销，比如bpe_v7_time要比bpe_v7慢10多秒，所以我这里只统计整体的合并时间。&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;数据&lt;/td&gt;
      &lt;td&gt;总时间(s)&lt;/td&gt;
      &lt;td&gt;统计词频时间(s)&lt;/td&gt;
      &lt;td&gt;合并时间(s)&lt;/td&gt;
      &lt;td&gt;其它&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1062/1035/1036&lt;/td&gt;
      &lt;td&gt;392/397/395&lt;/td&gt;
      &lt;td&gt;total: 606/573/577 max:6/6/6 update: 599/567/571&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1051/1017/1023&lt;/td&gt;
      &lt;td&gt;399/389/398&lt;/td&gt;
      &lt;td&gt;590/568/568&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_opt&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1021/1007/980&lt;/td&gt;
      &lt;td&gt;399/393/393&lt;/td&gt;
      &lt;td&gt;558/553/528&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;934/930/935&lt;/td&gt;
      &lt;td&gt;390/393/393&lt;/td&gt;
      &lt;td&gt;479/472/476&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8_v2&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;917/908/965&lt;/td&gt;
      &lt;td&gt;394/392/395&lt;/td&gt;
      &lt;td&gt;460/455/505&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8_v3&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;897/899/951&lt;/td&gt;
      &lt;td&gt;395/399/395&lt;/td&gt;
      &lt;td&gt;442/438/493&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;bpe_v8和bpe_v7的实现逻辑完全相同，我们可以对比得出cython版本的bpe_v8要比bpe_v7的合并时间少17%。bpe_v8_v2避免了复制&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;affected_tokens = word_tokens[start_idx: end_idx]&lt;/code&gt;，它的前两次运行要比bpe_v8快10多秒，但是第三次运行反而慢了30秒。这个原因不太清楚，也许是服务器的负载波动。bpe_v7_opt对bpe_v7做了new_pairs的删除优化，和它对比的是bpe_v8_v3，但是bpe_v8_v3还多做了bpe_v8_v2的避免复制的优化。bpe_v8_v3的平均时间是457秒，比bpe_v7_opt快了16%。&lt;/p&gt;

&lt;h2 id=&quot;5-把整个merge过程用cython实现&quot;&gt;5. 把整个merge过程用cython实现&lt;/h2&gt;

&lt;p&gt;更进一步，我们可以把整个merge过程都用cython实现，这能不能加快速度呢？对bpe_v8_v3进行重构我实现了&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_updater_v2.pyx&quot;&gt;bpe_updater_v2.pyx&lt;/a&gt;和&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v8_v4.py&quot;&gt;bpe_v8_v4.py&lt;/a&gt;。这里，我把merge过程封装成一个函数：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpdef void bpe_train_step2(int vocab_size,
                      pair_counts,
                      pair_strings,
                      vocabulary,
                      pair_to_words,
                      word_counts,
                      word_encodings,
                      merges,
                      pair_heap):

    cdef int size = len(vocabulary)
    while size &amp;lt; vocab_size:
        _merge_a_pair(pair_counts, pair_strings, vocabulary,
                                pair_to_words, word_counts, word_encodings,
                                merges, size, pair_heap)
        size += 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;测试结果如下：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;数据&lt;/td&gt;
      &lt;td&gt;总时间(s)&lt;/td&gt;
      &lt;td&gt;统计词频时间(s)&lt;/td&gt;
      &lt;td&gt;合并时间(s)&lt;/td&gt;
      &lt;td&gt;其它&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1062/1035/1036&lt;/td&gt;
      &lt;td&gt;392/397/395&lt;/td&gt;
      &lt;td&gt;total: 606/573/577 max:6/6/6 update: 599/567/571&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1051/1017/1023&lt;/td&gt;
      &lt;td&gt;399/389/398&lt;/td&gt;
      &lt;td&gt;590/568/568&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_opt&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1021/1007/980&lt;/td&gt;
      &lt;td&gt;399/393/393&lt;/td&gt;
      &lt;td&gt;558/553/528&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;934/930/935&lt;/td&gt;
      &lt;td&gt;390/393/393&lt;/td&gt;
      &lt;td&gt;479/472/476&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8_v2&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;917/908/965&lt;/td&gt;
      &lt;td&gt;394/392/395&lt;/td&gt;
      &lt;td&gt;460/455/505&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8_v3&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;897/899/951&lt;/td&gt;
      &lt;td&gt;395/399/395&lt;/td&gt;
      &lt;td&gt;442/438/493&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v8_v4&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;917/915/982&lt;/td&gt;
      &lt;td&gt;391/404/400&lt;/td&gt;
      &lt;td&gt;462/448/506&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;bpe_v8_v4比bpe_v8_v3还慢。原因在于fine_grained_pair_counter_diff函数我们通过cython的静态类型声明，把python的迭代器循环变成了c的循环，所以速度能够变快。但是其它部分的代码都是操作python的dict，这些代码用cython实现还是会回到python，这反而更慢。&lt;/p&gt;

&lt;h2 id=&quot;6-使用pypy&quot;&gt;6. 使用pypy&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://pypy.org/&quot;&gt;pypy&lt;/a&gt; 是 Python 语言的一个替代性实现。简单来说，它不是一个库或框架，而是一个完整的 Python 解释器，就像我们平时用的 CPython 一样。pypy 的最大亮点在于它内置了 JIT (Just-In-Time) 编译器。这是它和标准 CPython 解释器最大的区别，也是它速度快的原因。CPython将 Python 代码编译成字节码，然后由一个虚拟机逐条解释执行。这个过程比较慢。pypy同样先将 Python 代码编译成字节码。但是，当程序运行时，pypy 的 JIT 编译器会监控哪些代码被频繁执行。它会把这些“热点”代码直接编译成机器码，然后缓存起来。下次再遇到同样的代码时，pypy 就会直接执行高速的机器码，而不是重新解释字节码。pypy和Java的JVM是比较类似的。&lt;/p&gt;

&lt;p&gt;要把运行环境从CPython切换到pypy不需要修改任何代码，但是需要重新安装环境。由于我们是使用uv来管理环境的，切换环境非常简单，执行如下命令即可：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;deactivate
UV_PROJECT_ENVIRONMENT=.venv_pypy uv sync --python pypy@3.11
source .venv_pypy/bin/activate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;第一个是先从当前的虚拟环境中退出。然后用uv sync重新创建一个pypy 3.11的环境，为了不覆盖原来的.venv，我们使用环境变量UV_PROJECT_ENVIRONMENT，告诉uv新创建的虚拟环境存放在.venv_pypy目录下。最后source这个环境。&lt;/p&gt;

&lt;p&gt;我首先测试了bpe_v7，测试结果如下：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;数据&lt;/td&gt;
      &lt;td&gt;总时间(s)&lt;/td&gt;
      &lt;td&gt;统计词频时间(s)&lt;/td&gt;
      &lt;td&gt;合并时间(s)&lt;/td&gt;
      &lt;td&gt;其它&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1028/1053/1045&lt;/td&gt;
      &lt;td&gt;395/397/393&lt;/td&gt;
      &lt;td&gt;total: 575/589/590 max: 6/6/6 update: 569/583/583 make heap: 0.01/0.01 heap_push_time: 102/107/122&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7(pypy)&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;2047/1694/1913&lt;/td&gt;
      &lt;td&gt;1644/1306/1552&lt;/td&gt;
      &lt;td&gt;403/388/361&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;结果发现pypy的整体运行时间远远慢于CPython，从1000多秒增加到了2000多秒。不过pypy的merge时间却比CPython快了34%。为什么会出现前后速度不一致的情况呢？通过一系列测试我逐渐把问题锁定到了regex匹配的速度差异上，于是写了一个程序&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v2_time_pypy.py&quot;&gt;bpe_v2_time_pypy.py&lt;/a&gt;。这个程序只执行_pretokenize_and_count这个函数并且统计时间。下面是我在pypy3.11、CPython3.12和CPython3.11之下用单个CPU(num_counter=1)在tinystory数据集上测试的结果：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;数据&lt;/td&gt;
      &lt;td&gt;split_time(s)&lt;/td&gt;
      &lt;td&gt;match_time(s)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v2_pypy(pypy 3.11)&lt;/td&gt;
      &lt;td&gt;tiny_story&lt;/td&gt;
      &lt;td&gt;40&lt;/td&gt;
      &lt;td&gt;888&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v2_pypy(cpython 3.12) tiny_story&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;300&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v2_pypy(cpython 3.11) tiny_story&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;288&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;split_time和match_time分别统计regex.split和regex.finditer的时间：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;        for chunk in BPE_Trainer._chunk_documents_streaming(input_path):
            start_time = time.perf_counter()
            blocks = re.split(special_pattern, chunk)
            end_time = time.perf_counter()
            split_time += (end_time - start_time)
            for block in blocks:
                start_time = time.perf_counter()
                for match in re.finditer(pattern, block):
                    text = match.group(0)
                    text_len += len(text)
                end_time = time.perf_counter()
                match_time += (end_time - start_time)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;可以看到，pypy的版本要比CPython的慢很多。为了找到原因，我在第三方库regex的github上提交了一个issue&lt;a href=&quot;https://github.com/mrabarnett/mrab-regex/issues/586&quot;&gt;regex is much slower in pypy than cpython&lt;/a&gt;。根据mattip的回复，pypy慢的原因在于regex使用了CPython的C接口，而pypy只能通过模拟来实现类似CPython的这个接口，所以速度很慢。而且不只是慢，它的结果甚至都可能不正确。根据regex的文档：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;This module is targeted at CPython. It expects that all codepoints are the same width, so it won’t behave properly with pypy outside U+0000..U+007F because pypy stores strings as UTF-8.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;非常遗憾，pypy的JIT编译确实使得后面的merge过程变快了，但是由于第三方库regex不支持，我们用pypy整体更慢甚至无法实现。我们当然可以把train函数分成两部分，第一部分是_pretokenize_and_count，第二部分是merge。第一个部分用CPython，第二个部分用pypy，然后两个部分之间通过进程间通信来完成。但是进程间通信比较复杂，而且数据复制的开销也很大。&lt;/p&gt;

&lt;p&gt;所以pypy的最大问题其实是无法和CPython”兼容”，但是现实问题是很多高性能第三方库都会用到CPython的C接口。比如numpy，pypy无法直接加速 numpy 底层的 C 代码。相反，它通过一个名为 cpyext 的兼容层来与 numpy 的 C 扩展进行通信。这个兼容层是为了让 pypy 能够运行 CPython 的 C 扩展而设计的。这样单纯使用numpy的话pypy比CPython还要慢。&lt;/p&gt;

&lt;h2 id=&quot;7-总结&quot;&gt;7. 总结&lt;/h2&gt;

&lt;p&gt;通过pypy虽然能够加快python代码的执行，但是由于第三方库regex不支持，我们只能放弃。而通过cython，我们没有怎么修改python代码(不过为了模块调用进行了一定代码的重构)的情况下提升了速度。当然我们还可以进一步优化cython代码，比如使用c++的unordered_map替代python的dict。不过更好的方法是直接在c++语言里完成这些优化，然后把它封装成动态库供cython调用。这就是下一篇文章我们要研究的内容。&lt;/p&gt;

&lt;h2 id=&quot;本系列全部文章&quot;&gt;本系列全部文章&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/05/bpe-trainer-0/&quot;&gt;第0部分：简介&lt;/a&gt; 介绍bpe训练的基本算法和相关任务，并且介绍开发环境。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/07/bpe-trainer-1/&quot;&gt;第1部分：最简单实现&lt;/a&gt; bpe训练最简单的实现。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/08/bpe-trainer-2/&quot;&gt;第2部分：优化算法&lt;/a&gt; 实现pair_counts的增量更新。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/09/bpe-trainer-3/&quot;&gt;第3部分：并行分词和统计词频&lt;/a&gt; 使用multiprocessing实现多进程并行算法。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/10/bpe-trainer-4/&quot;&gt;第4部分：一次失败的并行优化&lt;/a&gt; 尝试用多进程并行计算max pair。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/12/bpe-trainer-5/&quot;&gt;第5部分：用C++实现Merge算法&lt;/a&gt; 用C++实现和Python等价的merge算法，并且比较std::unordered_map的两种遍历方式。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/15/bpe-trainer-6/&quot;&gt;第6部分：用OpenMP实现并行求最大&lt;/a&gt; 用OpenMP并行求pair_counts里最大pair。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/18/bpe-trainer-7/&quot;&gt;第7部分：使用flat hashmap替代std::unordered_map&lt;/a&gt; 使用flat hashmap来替代std::unordered_map。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/19/bpe-trainer-8/&quot;&gt;第8部分：实现细粒度更新&lt;/a&gt; 使用倒排索引实现pair_counts的细粒度更新算法。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/21/bpe-trainer-9/&quot;&gt;第9部分：使用堆来寻找最大pair&lt;/a&gt; 使用堆来求最大pair，提升性能。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/24/bpe-trainer-10/&quot;&gt;第10部分：使用cython和pypy来加速&lt;/a&gt; 使用cython和pypy来加速python代码。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/25/bpe-trainer-11/&quot;&gt;第11部分：使用cython封装c++代码&lt;/a&gt; 使用cython封装c++代码。&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 24 Sep 2025 00:00:00 +0000</pubDate>
        <link>http://fancyerii.github.io/2025/09/24/bpe-trainer-10/</link>
        <guid isPermaLink="true">http://fancyerii.github.io/2025/09/24/bpe-trainer-10/</guid>
        
        <category>cs336</category>
        
        <category>bpe tokenizer</category>
        
        
      </item>
    
      <item>
        <title>Implementing and Optimizing a BPE Tokenizer from Scratch—Part 9: Using a Heap to Find the Maximum Pair</title>
        <description>&lt;p&gt;This series of articles implements a subtask of Stanford’s CS336 Assignment 1: building an efficient training algorithm for a BPE Tokenizer. Through a series of optimizations, our algorithm’s training time on OpenWebText was reduced from over 10 hours to less than 10 minutes. This series explains these optimizations, including algorithmic improvements, data structure enhancements, parallelization with OpenMP, Cython optimization, and implementing key code in C++ along with its integration via Cython. This is the tenth article, where we use the heap data structure to replace the process of finding the maximum pair, thereby improving performance.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;strong&gt;Table of Content&lt;/strong&gt;&lt;/p&gt;
&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#1-problem-analysis&quot; id=&quot;markdown-toc-1-problem-analysis&quot;&gt;1. Problem Analysis&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-heap&quot; id=&quot;markdown-toc-2-heap&quot;&gt;2. Heap&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-using-a-heap-to-find-the-max&quot; id=&quot;markdown-toc-3-using-a-heap-to-find-the-max&quot;&gt;3. Using a Heap to Find the Max&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-pythons-heapq-module&quot; id=&quot;markdown-toc-4-pythons-heapq-module&quot;&gt;4. Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; Module&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-implementing-a-max-heap&quot; id=&quot;markdown-toc-5-implementing-a-max-heap&quot;&gt;5. Implementing a Max Heap&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-optimizing-maxheap_py-with-a-c-module&quot; id=&quot;markdown-toc-6-optimizing-maxheap_py-with-a-c-module&quot;&gt;6. Optimizing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheap_py&lt;/code&gt; with a C Module&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#7-using-heapq-to-implement-a-max-heap&quot; id=&quot;markdown-toc-7-using-heapq-to-implement-a-max-heap&quot;&gt;7. Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; to Implement a Max Heap&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#8-performance-testing-of-different-max-heap-implementations&quot; id=&quot;markdown-toc-8-performance-testing-of-different-max-heap-implementations&quot;&gt;8. Performance Testing of Different Max Heap Implementations&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#9-using-a-max-heap-to-find-the-max&quot; id=&quot;markdown-toc-9-using-a-max-heap-to-find-the-max&quot;&gt;9. Using a Max Heap to Find the Max&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#10-testing&quot; id=&quot;markdown-toc-10-testing&quot;&gt;10. Testing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#11-experiments-with-a-larger-vocab_size&quot; id=&quot;markdown-toc-11-experiments-with-a-larger-vocab_size&quot;&gt;11. Experiments with a Larger &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vocab_size&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#12-porting-the-max-heap-to-c&quot; id=&quot;markdown-toc-12-porting-the-max-heap-to-c&quot;&gt;12. Porting the Max Heap to C++&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#13-optimizing-_updated_affected_word_count&quot; id=&quot;markdown-toc-13-optimizing-_updated_affected_word_count&quot;&gt;13. Optimizing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_updated_affected_word_count&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#14-porting-the-optimization-to-c&quot; id=&quot;markdown-toc-14-porting-the-optimization-to-c&quot;&gt;14. Porting the Optimization to C++&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#full-series&quot; id=&quot;markdown-toc-full-series&quot;&gt;Full Series&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;1-problem-analysis&quot;&gt;1. Problem Analysis&lt;/h2&gt;

&lt;p&gt;In our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v5_time&lt;/code&gt; from above, the update time was just over 500 seconds, but the time for finding the max was over 30,000 seconds. How can we optimize this time? We previously tried a parallel algorithm for finding the max, but the Python version was not successful because the Python GIL prevents multiple threads from using multiple CPUs simultaneously, and multiprocessing involves a lot of inter-process communication. We then tried a C++ implementation with OpenMP for parallelism, which reduced the max time to 400 seconds with 32 threads. We then replaced &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt; with a faster flat hashmap, which brought the max time down to under 100 seconds without parallelism.&lt;/p&gt;

&lt;p&gt;However, let’s go back to Python. Besides optimizing data structures, is there a way to optimize the algorithm itself? Optimizing data structures means doing the same amount of work faster, while optimizing an algorithm means achieving the same goal with less work. Of course, the two are not completely separate; sometimes, a different algorithm requires designing an appropriate data structure.&lt;/p&gt;

&lt;p&gt;To find the maximum value in a set, we must iterate through the entire set, and this time cannot be reduced. This means the time for the first traversal cannot be reduced. However, the second traversal can be optimized because only a portion of the pair counts change between traversals. At the beginning, the affected words are numerous due to high-frequency terms, so relatively more pairs are affected. Later on, as word frequencies decrease, fewer words are affected. The simplest idea is to sort the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;. When some pair counts change, we would only need to re-sort those specific pairs. Since there are only two possibilities for changes in word frequency—counts of old pairs decrease, and counts of new pairs increase—we could also use some heuristic rules. For example, if a pair’s current rank is greater than 32,000 (our target vocabulary size) and its frequency has decreased, we can ignore it.&lt;/p&gt;

&lt;p&gt;But there is a better way to find the maximum value in a set: using a heap data structure.&lt;/p&gt;

&lt;h2 id=&quot;2-heap&quot;&gt;2. Heap&lt;/h2&gt;

&lt;p&gt;In computer science, “heap” has two completely different meanings: one refers to a data structure, and the other refers to a type of memory allocation. Interestingly, the counterpart to the heap as a memory allocation type is the stack, which also represents both a memory allocation method and a data structure. The stack as a data structure (LIFO) is closely related to the stack as memory allocation, as it leverages the LIFO principle for function calls and returns. However, the heap as a data structure and the heap as memory allocation have no connection; their only link is that someone, for some reason, used the same word for two entirely different concepts.&lt;/p&gt;

&lt;p&gt;Here, we’re focusing on the heap as a data structure. It’s typically used to implement priority queues and can also be used for heapsort. I won’t go into a detailed introduction of heaps; readers who are unfamiliar can find plenty of resources in any data structures and algorithms book or online, for example, on &lt;a href=&quot;https://en.wikipedia.org/wiki/Heap_\(data_structure\)&quot;&gt;Wikipedia&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;3-using-a-heap-to-find-the-max&quot;&gt;3. Using a Heap to Find the Max&lt;/h2&gt;

&lt;p&gt;Typically, when using a heap, we only use three main operations: converting an array into a heap (&lt;strong&gt;heapify&lt;/strong&gt;), popping the top element from the heap (&lt;strong&gt;heappop&lt;/strong&gt;), and pushing an element into the heap (&lt;strong&gt;heappush&lt;/strong&gt;). We first need to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapify&lt;/code&gt; to convert an array into a heap (satisfying the heap property: the root is larger/smaller than every element in its subtrees). Then, we can repeatedly call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappop&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt;, and the array will remain a heap after these operations.&lt;/p&gt;

&lt;p&gt;There’s a problem here: after finding the maximum (with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappop&lt;/code&gt;), we merge a pair. This decreases the count of some old pairs and introduces some new ones. Adding new pairs is not an issue; we just need to call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt;. But how do we modify the counts of old pairs and keep the structure a valid heap? Let’s look at an example:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         11
       /    \
      /      \
     9        4
   /   \     / \
  7     8   3   1 
 / \   / \
6  4  5   7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If we change 8 to 10, meaning an element has increased, we need to call a “sift up” operation (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;siftup&lt;/code&gt;) starting from 8:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         11
       /    \
      /      \
     10       4
   /   \     / \
  7     9   3   1 
 / \   / \
6  4  5   7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Conversely, if we change 8 to 6, we need to call a “sift down” operation (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;siftdown&lt;/code&gt;) starting from 8:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         11
       /    \
      /      \
     10       4
   /   \     / \
  7     7   3   1 
 / \   / \
6  4  5   6
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But there’s a problem: how do we find the element to be modified? Recall that our main data is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;, which is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict&lt;/code&gt;. However, a heap operates on a list. So we need to copy the elements from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_heap&lt;/code&gt; list. But a list doesn’t support fast lookups; if we had to sequentially scan the list to find an element for modification, it would be a counterproductive effort (we’ve already found the max).&lt;/p&gt;

&lt;p&gt;One solution is to store the index of the element in the list within the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; dictionary, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; would become:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pair -&amp;gt; (count, index_in_pair_heap)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, when we add a new pair to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;, we also add it to the correct position in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_heap&lt;/code&gt; using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt;. This would require the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt; function to not only add an element but also return its index in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_heap&lt;/code&gt;. This way, we can save the index in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;. Later, when the count of a pair changes (in our case, it will only decrease), we can find its position in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_heap&lt;/code&gt; via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; and then call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;siftdown&lt;/code&gt; on that element.&lt;/p&gt;

&lt;p&gt;This would require modifying &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt; and maintaining the relationship between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_heap&lt;/code&gt;, which makes the code quite complex. Readers who are interested can try to implement this algorithm.&lt;/p&gt;

&lt;p&gt;However, I’m using a different approach—&lt;strong&gt;lazy modification&lt;/strong&gt;. With this method, we do nothing when a pair’s count changes. We only check if its count has changed when we call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappop&lt;/code&gt;, which we can discover by comparing the heap element’s count with the value in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;. If it has changed (it will only get smaller, which is a very important assumption), we re-insert the new count using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt;. We then continue to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappop&lt;/code&gt; until we find an element whose count has not been modified. That element is the current maximum.&lt;/p&gt;

&lt;p&gt;This sounds complicated, so let’s walk through an example. Suppose our current heap is:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         11
       /    \
      /      \
     9        4
   /   \     / \
  7     8   3   1 
 / \   / \
6  4  5   7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The maximum should be 11, but let’s assume that due to a merge, its count has changed to 10. When we pop the pair with 11, we query &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; and find its latest count is 10. Since it’s smaller, we can’t be sure it’s the max. So, we first pop 11, which gives us:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;          9
       /    \
      /      \
     8        4
   /   \     / \
  7     7   3   1 
 / \   /
6  4  5 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we need to re-push 10, resulting in:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         10
       /    \
      /      \
     9        4
   /   \     / \
  7     8   3   1 
 / \   /  \
6  4  5    7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, we pop the current maximum, 10. This time, a query to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; reveals its count is up-to-date, so we’ve found the current maximum pair is 10.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important Note&lt;/strong&gt;: The crucial assumption that allows for this lazy update is that the count of an old pair will &lt;strong&gt;only decrease&lt;/strong&gt;. If this assumption were not true, for example, if our heap were:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         11
       /    \
      /      \
     9        4
   /   \     / \
  7     8   3   1 
 / \   / \
6  4  5   7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And we changed 1 to 12, we would have to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;siftup&lt;/code&gt; 1 immediately, otherwise the max we find would be 11, which is incorrect.&lt;/p&gt;

&lt;p&gt;Using this algorithm, we don’t need to maintain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_heap&lt;/code&gt; indices in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;, and we don’t need to call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;siftdown&lt;/code&gt;, which is a private function in Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_siftdown&lt;/code&gt;), making its use risky as it might not be available in future versions.&lt;/p&gt;

&lt;h2 id=&quot;4-pythons-heapq-module&quot;&gt;4. Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; Module&lt;/h2&gt;

&lt;p&gt;Python’s standard library provides the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; module. The main functions we need are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappop&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapify&lt;/code&gt;. Readers who are unfamiliar can refer to &lt;a href=&quot;https://realpython.com/python-heapq-module/&quot;&gt;The Python heapq Module: Using Heaps and Heappushs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, there is an issue here: we need a &lt;strong&gt;max heap&lt;/strong&gt;, but Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; module provides a &lt;strong&gt;min heap&lt;/strong&gt;. Later, when we analyze its code, we’ll see that it has already implemented a max heap internally. But for now, we need to discuss how we can use a min heap to achieve the functionality of a max heap.&lt;/p&gt;

&lt;p&gt;A common trick is to reverse the elements. For example, if the heap elements are positive integers, we can store their corresponding negative integers to simulate a max heap. Here’s an example:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;heapq&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;arr2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;arr2&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;heapq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heapify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arr2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;arr2&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;arr3&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;arr3&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But now we need to put tuples &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(count, pair_string[pair], pair)&lt;/code&gt; into the heap. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; is large, the tuple is large; if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; is the same, we compare the pair’s string and choose the larger one. The pair is included as the last element of the tuple for convenience.&lt;/p&gt;

&lt;p&gt;Following the above method, we can put &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-count&lt;/code&gt; into the heap. But what about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_strings[pair]&lt;/code&gt;? &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_strings[pair]&lt;/code&gt; is a tuple of bytes. If the bytes are of fixed length, since a byte’s range is 0-255, we can reverse it by subtracting each byte from 255. For example:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;b1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;us&apos;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;b2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;ua&apos;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;b1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b2&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;c1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;255&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;c1&lt;/span&gt;
&lt;span class=&quot;sa&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x8a\x8c&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;c2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;255&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;c1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c2&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But if the strings are of variable length, this will cause problems. For example:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;b1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;us&apos;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;b2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;usb&apos;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;b1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b2&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;c1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;255&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;c2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;255&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;c1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c2&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b1&lt;/code&gt; has one less character than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b2&lt;/code&gt;. After reversing by subtracting from 255, the leading &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b&apos;us&apos;&lt;/code&gt; parts are the same, but regardless of reversal, the shorter string is always considered smaller than the longer one.&lt;/p&gt;

&lt;p&gt;Therefore, we need a max heap.&lt;/p&gt;

&lt;h2 id=&quot;5-implementing-a-max-heap&quot;&gt;5. Implementing a Max Heap&lt;/h2&gt;

&lt;p&gt;We can slightly modify Python’s built-in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; module to turn it into a max heap. We’ll copy the source code of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; and modify it into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheap_py.py&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; module has many functions, but we only need to keep &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappop&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapify&lt;/code&gt;. These three functions, in turn, depend on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_siftdown&lt;/code&gt; (which I’ve renamed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_siftdown_max&lt;/code&gt;) and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_siftup&lt;/code&gt; (renamed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_siftup_max&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;I won’t go into the full code, but here’s a comparison of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_siftdown_max&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_siftdown&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_siftdown_max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;startpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;Maxheap variant of _siftdown&apos;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Follow the path to the root, moving parents down until finding a place
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# newitem fits.
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;startpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_siftdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;startpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Follow the path to the root, moving parents down until finding a place
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# newitem fits.
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;startpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The only difference is the single line: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;if parent &amp;lt; newitem&quot;&lt;/code&gt; versus &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;if newitem &amp;lt; parent&quot;&lt;/code&gt;. This assumes that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;newitem&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parent&lt;/code&gt; values have an overloaded or implemented &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__lt__&lt;/code&gt; operator, so even when finding the larger element, it’s done by swapping the order and using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__lt__&lt;/code&gt; function.&lt;/p&gt;

&lt;h2 id=&quot;6-optimizing-maxheap_py-with-a-c-module&quot;&gt;6. Optimizing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheap_py&lt;/code&gt; with a C Module&lt;/h2&gt;

&lt;p&gt;Readers who are not interested in C module development can skip this section.&lt;/p&gt;

&lt;p&gt;If we compare the speed of our version to the CPython version, ours is much slower because the CPython version internally calls a corresponding C module. If we look closely at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; source code, we’ll find:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# If available, use C implementation
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;_heapq&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;ImportError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;_heapq&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_heapreplace_max&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;ImportError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;_heapq&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_heapify_max&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;ImportError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;_heapq&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_heappop_max&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;ImportError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This means it will (attempt to) call the corresponding C module implementation. The specific code is in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_heapqmodule.c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I’ve copied this to implement the max heap functionality. The full code is in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_maxheapqmodule.c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let’s look at just one function, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;siftdown&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;siftdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PyListObject&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Py_ssize_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;startpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Py_ssize_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyObject&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Py_ssize_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cmp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PyList_Check&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyList_GET_SIZE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyErr_SetString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PyExc_IndexError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;index out of range&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* Follow the path to the root, moving parents down until finding
       a place newitem fits. */&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_PyList_ITEMS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;startpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Py_INCREF&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Py_INCREF&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cmp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyObject_RichCompareBool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Py_GT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Py_DECREF&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Py_DECREF&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cmp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyList_GET_SIZE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyErr_SetString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PyExc_RuntimeError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;list changed size during iteration&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cmp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_PyList_ITEMS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parentpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The two comparison codes have only a one-line difference:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;cmp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyObject_RichCompareBool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Py_GT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cmp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyObject_RichCompareBool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;newitem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Py_LT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As mentioned before, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PyObject_RichCompareBool&lt;/code&gt; compares the first two arguments based on the third. A return value less than 0 indicates an error; a return value greater than 0 means the third argument’s comparison (e.g., greater than/less than) is true; and a return value of 0 means it’s false.&lt;/p&gt;

&lt;p&gt;Therefore, to change a min heap to a max heap, we just need to change the comparison operator from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Py_LT&lt;/code&gt; (less than) to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Py_GT&lt;/code&gt; (greater than).&lt;/p&gt;

&lt;p&gt;Of course, there are other minor details to modify. For example, I’ve changed the module name to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheapqc&lt;/code&gt;, so the corresponding code needs to be updated:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyModuleDef&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_heapqmodule&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyModuleDef_HEAD_INIT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;maxheapqc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;module_doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heapq_methods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heapq_slots&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;NULL&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;PyMODINIT_FUNC&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;PyInit_maxheapqc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyModuleDef_Init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_heapqmodule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To compile this C module, we need to modify &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setup.py&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;include_path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sysconfig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;include&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;internal_include_path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;include_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;internal&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;internal_include_path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;project_root&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;abspath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__file__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;maxheapqc_module&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Extension&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cs336_basics.maxheapqc&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sources&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cs336_basics/_maxheapqmodule.c&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extra_compile_args&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;-I&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;internal_include_path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;maxheapqc&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;1.0&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;maxheapqc&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cs336_basics&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ext_modules&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maxheapqc_module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When we compile &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_maxheapqmodule.c&lt;/code&gt;, it depends on:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#include&lt;/span&gt; &lt;span class=&quot;cpf&quot;&gt;&quot;Python.h&quot;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;
#include&lt;/span&gt; &lt;span class=&quot;cpf&quot;&gt;&quot;pycore_list.h&quot;&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt; &lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Python.h&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setuptools&lt;/code&gt; will include it for us. Its location is typically something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/include/python3.12/Python.h&lt;/code&gt;. But our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_maxheapqmodule.c&lt;/code&gt; also needs to operate on lists via the C API, which requires &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pycore_list.h&lt;/code&gt;. This file is located at a path like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/include/python3.12/internal/pycore_list.h&lt;/code&gt;. We don’t want to hard-code this path, and the same system might have multiple Python versions and environments like Conda. So we can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sysconfig.get_path(&apos;include&apos;)&lt;/code&gt; to get the current Python interpreter’s path and then find &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;internal&lt;/code&gt; within it.&lt;/p&gt;

&lt;p&gt;We need to add this path during compilation using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-I&lt;/code&gt; flag:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;maxheapqc_module&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Extension&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cs336_basics.maxheapqc&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sources&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cs336_basics/_maxheapqmodule.c&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extra_compile_args&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;-I&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;internal_include_path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-I&lt;/code&gt; parameter seems to be the way &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gcc&lt;/code&gt; adds header files. I’m not sure if it’s universal for non-Linux systems or other compilers. If you are using a different compiler, please refer to its manual for adding appropriate header file paths.&lt;/p&gt;

&lt;p&gt;Run the following command to compile the C module:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python setup.py build_ext &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After compilation, you’ll get a file like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cs336_basics/maxheapqc.cpython-312-x86_64-linux-gnu.so&lt;/code&gt;. We can then write a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheapq.py&lt;/code&gt; to call it.&lt;/p&gt;

&lt;p&gt;The code for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheapq.py&lt;/code&gt; is similar to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq.py&lt;/code&gt;; it implements max heap functionality in Python and attempts to load &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheapqc&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# If available, use C implementation
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;cs336_basics.maxheapqc&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;load c!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;ImportError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;cs336_basics.maxheapqc&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_heapreplace_max&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;ImportError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;cs336_basics.maxheapqc&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_heapify_max&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;ImportError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;cs336_basics.maxheapqc&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_heappop_max&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;ImportError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;7-using-heapq-to-implement-a-max-heap&quot;&gt;7. Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; to Implement a Max Heap&lt;/h2&gt;

&lt;p&gt;In fact, if we read the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; source code, we’ll find that it already implements &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_heapify_max&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_siftdown_max&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_siftup_max&lt;/code&gt;. Among these, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_heapify_max&lt;/code&gt; has a corresponding C module implementation and is very fast. However, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_siftdown_max&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_siftup_max&lt;/code&gt; are still implemented in Python. We can look &lt;a href=&quot;https://github.com/python/cpython/blob/3.12/Modules/_heapqmodule.c%23L540&quot;&gt;here&lt;/a&gt; to find which functions have C implementations:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PyMethodDef&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heapq_methods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_HEAPQ_HEAPPUSH_METHODDEF&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_HEAPQ_HEAPPUSHPOP_METHODDEF&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_HEAPQ_HEAPPOP_METHODDEF&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_HEAPQ_HEAPREPLACE_METHODDEF&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_HEAPQ_HEAPIFY_METHODDEF&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_HEAPQ__HEAPPOP_MAX_METHODDEF&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_HEAPQ__HEAPIFY_MAX_METHODDEF&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_HEAPQ__HEAPREPLACE_MAX_METHODDEF&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;cm&quot;&gt;/* sentinel */&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can use these three functions from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapq&lt;/code&gt; to implement a max heap. The code is in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheap_heapq.py&lt;/code&gt;. Let’s take a look:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;heappush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heapq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_siftdown_max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;heappop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Maxheap version of a heappop.&quot;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lastelt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# raises appropriate IndexError if heap is empty
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;returnitem&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lastelt&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heapq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_siftup_max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;returnitem&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lastelt&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;heapify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;heapq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_heapify_max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapify&lt;/code&gt; calls the C interface and is fast, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappop&lt;/code&gt; are still Python implementations.&lt;/p&gt;

&lt;h2 id=&quot;8-performance-testing-of-different-max-heap-implementations&quot;&gt;8. Performance Testing of Different Max Heap Implementations&lt;/h2&gt;

&lt;p&gt;I’ve written a simple test script, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test_heap_speed.py&lt;/code&gt;. The results are as follows:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;maxheapq_py: 40.50480842590332
maxheap_heapq: 4.577162265777588
maxheapq: 4.334884405136108
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The majority of the time here is spent on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapify&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheapq_py&lt;/code&gt; is implemented in Python, so it’s much slower.&lt;/p&gt;

&lt;p&gt;And &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test_heap_speed2.py&lt;/code&gt; primarily tests &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappop&lt;/code&gt;. The results are as follows:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;maxheapq_py: 1.892250157892704
maxheap_heapq: 1.8967562650796026
maxheapq: 0.3524886401137337
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As you can see, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheapq&lt;/code&gt; is much faster than the Python implementations.&lt;/p&gt;

&lt;h2 id=&quot;9-using-a-max-heap-to-find-the-max&quot;&gt;9. Using a Max Heap to Find the Max&lt;/h2&gt;

&lt;p&gt;The complete code is in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v6.py&lt;/code&gt;. Let’s just look at the differences between it and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v5.py&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;cs336_basics&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maxheap_py&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maxheap&lt;/span&gt;

&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vocab_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;special_tokens&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maxheap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heappush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_strings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt;  &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt;  &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt;  &lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;First, we import &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheap_py&lt;/code&gt;, and for easy switching between different max heap implementations, I’ve aliased it as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheap&lt;/code&gt;. Since all different max heaps have the same interface, switching to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheapq&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheap_heapq&lt;/code&gt; only requires changing this one line of code.&lt;/p&gt;

&lt;p&gt;After the initial &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; are calculated, we need to construct a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_heap&lt;/code&gt;. This is done by continuously calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheap.heappush&lt;/code&gt;. Another implementation method is to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapify&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_strings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maxheap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heapify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Both methods will eventually build a heap, but their results may not be exactly the same. Theoretically, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heapify&lt;/code&gt; method is faster, but actual testing shows the time difference is minimal, as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; starts with fewer than 20,000 entries.&lt;/p&gt;

&lt;p&gt;Next is the main modification: finding the current maximum pair using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappop&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;staticmethod&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_merge_a_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_strings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vocabulary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_to_words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;word_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;word_encodings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merges&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt;  &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;string_priority&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merge_pair&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maxheap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heappop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt;  &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# check pair validity
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merge_pair&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;merge_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merge_pair&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# update count (lazily)
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maxheap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heappush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;merge_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;string_priority&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;merge_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# no valid pairs found
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The algorithm’s implementation is as described earlier: first, pop the current maximum &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge_pair&lt;/code&gt; from the top of the heap. Then, check if its count has been updated by comparing it to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts[merge_pair]&lt;/code&gt;. If the count hasn’t been updated, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge_pair&lt;/code&gt; is the current maximum, and we break the loop. If it has been updated and the new count is greater than 0 (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge_pair in pair_counts&lt;/code&gt;), we re-insert the new count into the heap using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt;. We then continue the loop to find the maximum pair.&lt;/p&gt;

&lt;p&gt;The final change is that whenever a new pair is created during a merge, we also need to add it to the heap:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;staticmethod&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_updated_affected_word_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;merge_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;affected_words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;word_encodings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;word_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_to_words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_strings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vocabulary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;



&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_pair&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_pairs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_pair&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_strings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_strings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vocabulary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vocabulary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]])&lt;/span&gt;

&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maxheap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heappush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_strings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;10-testing&quot;&gt;10. Testing&lt;/h2&gt;

&lt;p&gt;To test the time, I also implemented &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v6_time.py&lt;/code&gt;. The test results are as follows:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Version&lt;/th&gt;
      &lt;th&gt;Data&lt;/th&gt;
      &lt;th&gt;Total Time (s)&lt;/th&gt;
      &lt;th&gt;Word Count Time (s)&lt;/th&gt;
      &lt;th&gt;Merge Time (s)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v5_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;34333/34853/35804&lt;/td&gt;
      &lt;td&gt;401/390/401&lt;/td&gt;
      &lt;td&gt;total:33879/34401/35347 max:33353/33820/34816 update:525/579/530&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v6_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1036/1107/1046&lt;/td&gt;
      &lt;td&gt;395/395/398&lt;/td&gt;
      &lt;td&gt;total: 576/641/591 max:6/7/6 update: 570/633/584&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;After using a heap to find the max, the merge time decreased from over 30,000 seconds to just over 570 seconds. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt; time is over 100 seconds. Can we optimize it with a faster max heap?&lt;/p&gt;

&lt;p&gt;By changing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheap_py&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheap_heapq&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxheapq&lt;/code&gt;, we get &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7.py&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7_maxheapc.py&lt;/code&gt;. The test results are as follows:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Version&lt;/th&gt;
      &lt;th&gt;Data&lt;/th&gt;
      &lt;th&gt;Total Time (s)&lt;/th&gt;
      &lt;th&gt;Word Count Time (s)&lt;/th&gt;
      &lt;th&gt;Merge Time (s)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v5_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;34333/34853/35804&lt;/td&gt;
      &lt;td&gt;401/390/401&lt;/td&gt;
      &lt;td&gt;total:33879/34401/35347 max:33353/33820/34816 update:525/579/530&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v6_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1036/1107/1046&lt;/td&gt;
      &lt;td&gt;395/395/398&lt;/td&gt;
      &lt;td&gt;total: 576/641/591 max:6/7/6 update: 570/633/584&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1062/1035/1036&lt;/td&gt;
      &lt;td&gt;392/397/395&lt;/td&gt;
      &lt;td&gt;total: 606/573/577 max:6/6/6 update: 599/567/571&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_maxheapc_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1069/1017/1011&lt;/td&gt;
      &lt;td&gt;400/401/399&lt;/td&gt;
      &lt;td&gt;total: 606/556/555 max: 3/3/3 update: 602/552/552&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Using a faster heap implementation did not speed things up. I speculate this is because the time for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt; accounts for a relatively small proportion of the total update time, so a minor change in its time doesn’t have a large impact on the overall performance.&lt;/p&gt;

&lt;h2 id=&quot;11-experiments-with-a-larger-vocab_size&quot;&gt;11. Experiments with a Larger &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vocab_size&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;If we compare &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7&lt;/code&gt; with the C++ version &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8&lt;/code&gt;:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Version&lt;/th&gt;
      &lt;th&gt;Data&lt;/th&gt;
      &lt;th&gt;Total Time (sec)&lt;/th&gt;
      &lt;th&gt;Word Count Time (sec)&lt;/th&gt;
      &lt;th&gt;Merge Time (sec)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1028/1053/1045&lt;/td&gt;
      &lt;td&gt;395/397/393&lt;/td&gt;
      &lt;td&gt;total: 575/589/590 max: 6/6/6 update: 569/583/583 make heap: 0.01/0.01 heap_push_time: 102/107/122&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Program&lt;/th&gt;
      &lt;th&gt;Hash Function&lt;/th&gt;
      &lt;th&gt;Total Time (sec)&lt;/th&gt;
      &lt;th&gt;Update Time (sec)&lt;/th&gt;
      &lt;th&gt;Max Time (sec)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The total merge time for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7&lt;/code&gt; is around 600 seconds, while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8&lt;/code&gt; is around 260 seconds. But what if we increase the number of merges? Since the vocabularies of modern large models are getting larger, let’s test the results for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vocab_size&lt;/code&gt; of 64,000 and 96,000:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Version&lt;/th&gt;
      &lt;th&gt;Data&lt;/th&gt;
      &lt;th&gt;Total Time (s)&lt;/th&gt;
      &lt;th&gt;Word Count Time (s)&lt;/th&gt;
      &lt;th&gt;Merge Time (s)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1028/1053/1045&lt;/td&gt;
      &lt;td&gt;395/397/393&lt;/td&gt;
      &lt;td&gt;total: 575/589/590 max: 6/6/6 update: 569/583/583 make heap: 0.01/0.01 heap_push_time: 102/107/122&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time2&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1111/1174/1100&lt;/td&gt;
      &lt;td&gt;393/420/403&lt;/td&gt;
      &lt;td&gt;total: 655/686/639 max: 9/10/10 update: 645/675/628 make heap: 0.01/0.01/0.01 heap_push_time: 128/157/108&lt;/td&gt;
      &lt;td&gt;vocab_size=64000&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time2&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1129/1130/1123&lt;/td&gt;
      &lt;td&gt;394/406/393&lt;/td&gt;
      &lt;td&gt;total: 675/666/670 max: 13/12/12 update: 661/653/657 make heap: 0.01/0.01/0.01 heap_push_time: 143/152/120&lt;/td&gt;
      &lt;td&gt;vocab_size=96000&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;As you can see, because the later parts deal with low-frequency words and pairs, the time barely changes. Now let’s look at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8&lt;/code&gt;:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Program&lt;/th&gt;
      &lt;th&gt;Hash Function&lt;/th&gt;
      &lt;th&gt;Total Time (sec)&lt;/th&gt;
      &lt;th&gt;Update Time (sec)&lt;/th&gt;
      &lt;th&gt;Max Time (sec)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;413/414/406&lt;/td&gt;
      &lt;td&gt;233/227/223&lt;/td&gt;
      &lt;td&gt;179/187/182&lt;/td&gt;
      &lt;td&gt;64k&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;664/593/606&lt;/td&gt;
      &lt;td&gt;305/255/269&lt;/td&gt;
      &lt;td&gt;358/338/337&lt;/td&gt;
      &lt;td&gt;96k&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The time for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8&lt;/code&gt; gradually increases. This is because as the number of pairs in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; grows, the time to find the max will only increase, not decrease.&lt;/p&gt;

&lt;h2 id=&quot;12-porting-the-max-heap-to-c&quot;&gt;12. Porting the Max Heap to C++&lt;/h2&gt;

&lt;p&gt;We can also port this algorithm to C++. The C++ standard library’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;algorithm&amp;gt;&lt;/code&gt; header has functions like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::make_heap&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::push_heap&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::pop_heap&lt;/code&gt;, which can provide the same functionality as Python. The C++ heap is a max heap by default, so we just need to overload &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;operator&amp;lt;&lt;/code&gt;. We can define:&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HeapItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;std&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;std&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;std&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;operator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HeapItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;other&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can refer to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test_max_heap.cpp&lt;/code&gt; for how to use the standard library.&lt;/p&gt;

&lt;p&gt;I didn’t use the standard library here; instead, I rewrote the Python version in C++ and tested its speed. It seems to be slightly faster than the standard library. The full code for the max heap is in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_heap.cpp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Based on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained&lt;/code&gt;, I’ve implemented &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap.cpp&lt;/code&gt;. Based on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8_set&lt;/code&gt;, I’ve implemented &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set.cpp&lt;/code&gt;. Based on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8_set9&lt;/code&gt;, I’ve implemented &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9.cpp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The implementation logic is completely consistent with the Python version. Readers who are interested can read the code themselves. Below are the comparison test results:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Program&lt;/th&gt;
      &lt;th&gt;Hash Function&lt;/th&gt;
      &lt;th&gt;Total Time (sec)&lt;/th&gt;
      &lt;th&gt;Update Time (sec)&lt;/th&gt;
      &lt;th&gt;Max Time (sec)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8_set&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;192/192/194&lt;/td&gt;
      &lt;td&gt;117/117/117&lt;/td&gt;
      &lt;td&gt;75/75/77&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8_set9&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;168/170/171&lt;/td&gt;
      &lt;td&gt;107/108/109&lt;/td&gt;
      &lt;td&gt;61/62/61&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;200/228/211&lt;/td&gt;
      &lt;td&gt;194/220/208&lt;/td&gt;
      &lt;td&gt;2/3/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;199/200/210&lt;/td&gt;
      &lt;td&gt;193/195/203&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;139/122/128&lt;/td&gt;
      &lt;td&gt;136/118/123&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set9&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;111/121/122&lt;/td&gt;
      &lt;td&gt;109/116/116&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;As you can see, the time to find the max using a heap is less than 2 seconds. Now let’s look at its performance with vocabularies of 64,000 and 96,000:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Program&lt;/th&gt;
      &lt;th&gt;Hash Function&lt;/th&gt;
      &lt;th&gt;Total Time (sec)&lt;/th&gt;
      &lt;th&gt;Update Time (sec)&lt;/th&gt;
      &lt;th&gt;Max Time (sec)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;413/414/406&lt;/td&gt;
      &lt;td&gt;233/227/223&lt;/td&gt;
      &lt;td&gt;179/187/182&lt;/td&gt;
      &lt;td&gt;64k&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;664/593/606&lt;/td&gt;
      &lt;td&gt;305/255/269&lt;/td&gt;
      &lt;td&gt;358/338/337&lt;/td&gt;
      &lt;td&gt;96k&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;199/200/210&lt;/td&gt;
      &lt;td&gt;193/195/203&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;224/228/227&lt;/td&gt;
      &lt;td&gt;216/218/217&lt;/td&gt;
      &lt;td&gt;3/3/3&lt;/td&gt;
      &lt;td&gt;64k&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;249/255/240&lt;/td&gt;
      &lt;td&gt;239/238/230&lt;/td&gt;
      &lt;td&gt;5/6/4&lt;/td&gt;
      &lt;td&gt;96k&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;By comparing them, we can see that for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8&lt;/code&gt; algorithm, the update time only increases by about 20 seconds when the vocabulary increases from 32k to 64k, and the max time is almost unchanged. In contrast, the max time for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8&lt;/code&gt; doubles.&lt;/p&gt;

&lt;h2 id=&quot;13-optimizing-_updated_affected_word_count&quot;&gt;13. Optimizing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_updated_affected_word_count&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;If you carefully read the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_updated_affected_word_count&lt;/code&gt; function and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fine_grained_pair_counter_diff&lt;/code&gt;, you can see there’s redundancy between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new_pairs&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff_pairs&lt;/code&gt;. Recall that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff_pairs&lt;/code&gt; records the change in a pair’s count: a value greater than zero indicates an increase (new pair), and a value less than zero indicates a decrease (old pair). &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new_pairs&lt;/code&gt; represents all affected pairs (including pairs that may not have changed, which happens when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge_pair&lt;/code&gt; appears multiple times with other pairs in between). However, based on our previous algorithm, we only need to add new pairs to the heap with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt;, and old pairs can be updated lazily. So, we can remove &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new_pairs&lt;/code&gt;, and any pair in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff_pairs&lt;/code&gt; with a count &amp;gt; 0 must be a new pair.&lt;/p&gt;

&lt;p&gt;This leads to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7_maxheapc_opt_time.py&lt;/code&gt;. Other versions can be modified similarly.&lt;/p&gt;

&lt;p&gt;The main code changes are:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;staticmethod&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_updated_affected_word_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;merge_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;affected_words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;word_encodings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;word_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_to_words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_strings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vocabulary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# we may update/delete words when iterate it.
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;affected_words&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;affected_words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;diff_pairs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;defaultdict&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BPE_Trainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fine_grained_pair_counter_diff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;affected_words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;word_encodings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;word_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;merge_pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;diff_pairs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_to_words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;diff_pairs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# new pair
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_strings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vocabulary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vocabulary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]])&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maxheap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heappush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair_heap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_strings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt;  &lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# should not less than 0!
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;k&quot;&gt;del&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;err&quot;&gt; &lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pair_to_words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BPE_Trainer.fine_grained_pair_counter_diff&lt;/code&gt; no longer needs to take &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new_pairs&lt;/code&gt; as an output parameter. When iterating through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff_pairs&lt;/code&gt;, we check the count; if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count &amp;gt; 0&lt;/code&gt;, it’s a new pair, and we save its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_strings&lt;/code&gt; and add it to the heap with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heappush&lt;/code&gt;.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Version&lt;/th&gt;
      &lt;th&gt;Data&lt;/th&gt;
      &lt;th&gt;Total Time (s)&lt;/th&gt;
      &lt;th&gt;Word Count Time (s)&lt;/th&gt;
      &lt;th&gt;Merge Time (s)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v5_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;34333/34853/35804&lt;/td&gt;
      &lt;td&gt;401/390/401&lt;/td&gt;
      &lt;td&gt;total:33879/34401/35347 max:33353/33820/34816 update:525/579/530&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v6_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1036/1107/1046&lt;/td&gt;
      &lt;td&gt;395/395/398&lt;/td&gt;
      &lt;td&gt;total: 576/641/591 max:6/7/6 update: 570/633/584&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1062/1035/1036&lt;/td&gt;
      &lt;td&gt;392/397/395&lt;/td&gt;
      &lt;td&gt;total: 606/573/577 max:6/6/6 update: 599/567/571&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_maxheapc_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1069/1017/1011&lt;/td&gt;
      &lt;td&gt;400/401/399&lt;/td&gt;
      &lt;td&gt;total: 606/556/555 max: 3/3/3 update: 602/552/552&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_maxheapc_opt_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;984/965/1000&lt;/td&gt;
      &lt;td&gt;394/394/403&lt;/td&gt;
      &lt;td&gt;total: 532/514/538 max: 0.8/0.8/0.9 update: 531/513/537&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7_maxheapc_opt_time&lt;/code&gt; is about 7% faster than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v7_maxheapc_time&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;14-porting-the-optimization-to-c&quot;&gt;14. Porting the Optimization to C++&lt;/h2&gt;

&lt;p&gt;The optimized versions are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_opt.cpp&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_opt.cpp&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set_opt.cpp&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Test results on the OpenWeb dataset:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Program&lt;/th&gt;
      &lt;th&gt;Hash Function&lt;/th&gt;
      &lt;th&gt;Total Time (sec)&lt;/th&gt;
      &lt;th&gt;Update Time (sec)&lt;/th&gt;
      &lt;th&gt;Max Time (sec)&lt;/th&gt;
      &lt;th&gt;Other&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;200/228/211&lt;/td&gt;
      &lt;td&gt;194/220/208&lt;/td&gt;
      &lt;td&gt;2/3/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_opt&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;190/192/190&lt;/td&gt;
      &lt;td&gt;187/189/187&lt;/td&gt;
      &lt;td&gt;0/0/0&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;199/200/210&lt;/td&gt;
      &lt;td&gt;193/195/203&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_opt&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;188/213/192&lt;/td&gt;
      &lt;td&gt;185/209/189&lt;/td&gt;
      &lt;td&gt;0/0/0&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;139/122/128&lt;/td&gt;
      &lt;td&gt;136/118/123&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set_opt&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;110/130/110&lt;/td&gt;
      &lt;td&gt;108/128/108&lt;/td&gt;
      &lt;td&gt;0/0/0&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set9&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;111/121/122&lt;/td&gt;
      &lt;td&gt;109/116/116&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;105/102/104&lt;/td&gt;
      &lt;td&gt;102/100/101&lt;/td&gt;
      &lt;td&gt;0/0/0&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt&lt;/code&gt; is 11% faster in update time than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set_opt&lt;/code&gt; is 8% faster than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set&lt;/code&gt;. This conclusion is consistent with the Python version.&lt;/p&gt;

&lt;h2 id=&quot;full-series&quot;&gt;Full Series&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/05/bpe-trainer-0_en/&quot;&gt;Part 0: Introduction&lt;/a&gt; Introduces the basic BPE training algorithm and related tasks, as well as the development environment.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/07/bpe-trainer-1_en/&quot;&gt;Part 1: The Simplest Implementation&lt;/a&gt; The simplest implementation of BPE training.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/08/bpe-trainer-2_en/&quot;&gt;Part 2: Optimized Algorithm&lt;/a&gt; Implements incremental updates for pair_counts.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/09/bpe-trainer-3_en/&quot;&gt;Part 3: Parallel Tokenization and Frequency Counting&lt;/a&gt; Uses multiprocessing to implement a multi-process parallel algorithm.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/10/bpe-trainer-4_en/&quot;&gt;Part 4: A Failed Parallel Optimization&lt;/a&gt; An attempt to parallelize the max pair calculation using multiple processes.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/12/bpe-trainer-5_en/&quot;&gt;Part 5: Implementing the Merge Algorithm in C++&lt;/a&gt; Implements a C++ merge algorithm equivalent to the Python version, and compares two ways of iterating through std::unordered_map.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/15/bpe-trainer-6_en/&quot;&gt;Part 6: Parallelizing the Max Pair Search with OpenMP&lt;/a&gt; Uses OpenMP to find the max pair in pair_counts in parallel.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/18/bpe-trainer-7_en/&quot;&gt;Part 7: Using Flat Hashmap to Replace std::unordered_map&lt;/a&gt; Uses flat hashmap to replace std::unordered_map.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/19/bpe-trainer-8_en/&quot;&gt;Part 8: Implementing Fine-Grained Updates&lt;/a&gt; Implements a fine-grained update algorithm for pair_counts using an inverted index.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/21/bpe-trainer-9_en/&quot;&gt;Part 9: Using a Heap to Find the Max Pair&lt;/a&gt; Uses a heap to find the max pair and improve performance.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/24/bpe-trainer-10_en/&quot;&gt;Part 10: Using Cython and PyPy for Acceleration&lt;/a&gt; Uses Cython and PyPy to accelerate Python code.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/25/bpe-trainer-11_en/&quot;&gt;Part 11: Wrapping C++ Code with Cython&lt;/a&gt; Wraps C++ code using Cython.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 21 Sep 2025 00:00:00 +0000</pubDate>
        <link>http://fancyerii.github.io/2025/09/21/bpe-trainer-9_en/</link>
        <guid isPermaLink="true">http://fancyerii.github.io/2025/09/21/bpe-trainer-9_en/</guid>
        
        <category>cs336</category>
        
        <category>bpe tokenizer</category>
        
        
      </item>
    
      <item>
        <title>动手实现和优化BPE Tokenizer的训练——第9部分：使用堆来寻找最大pair</title>
        <description>&lt;p&gt;本系列文章完成Stanford CS336作业1的一个子任务——实现BPE Tokenizer的高效训练算法。通过一系列优化，我们的算法在OpenWebText上的训练时间从最初的10多个小时优化到小于10分钟。本系列文章解释这一系列优化过程，包括：算法的优化，数据结构的优化，并行(openmp)优化，cython优化，用c++实现关键代码和c++库的cython集成等内容。本文是第十篇，使用堆(heap)这个数据结构来替代求最大pair，提升性能。&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#1-问题分析&quot; id=&quot;markdown-toc-1-问题分析&quot;&gt;1. 问题分析&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-堆heap&quot; id=&quot;markdown-toc-2-堆heap&quot;&gt;2. 堆(heap)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-使用堆求max&quot; id=&quot;markdown-toc-3-使用堆求max&quot;&gt;3. 使用堆求max&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-python的heapq模块&quot; id=&quot;markdown-toc-4-python的heapq模块&quot;&gt;4. Python的heapq模块&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-实现一个大堆&quot; id=&quot;markdown-toc-5-实现一个大堆&quot;&gt;5. 实现一个大堆&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-用c模块优化maxheap_py&quot; id=&quot;markdown-toc-6-用c模块优化maxheap_py&quot;&gt;6. 用c模块优化maxheap_py&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#7-用heapq实现大堆&quot; id=&quot;markdown-toc-7-用heapq实现大堆&quot;&gt;7. 用heapq实现大堆&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#8-不同的大堆实现的性能测试&quot; id=&quot;markdown-toc-8-不同的大堆实现的性能测试&quot;&gt;8. 不同的大堆实现的性能测试&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#9-用大堆求max&quot; id=&quot;markdown-toc-9-用大堆求max&quot;&gt;9. 用大堆求max&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#10-测试&quot; id=&quot;markdown-toc-10-测试&quot;&gt;10. 测试&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#11-更大vocab_size的实验&quot; id=&quot;markdown-toc-11-更大vocab_size的实验&quot;&gt;11. 更大vocab_size的实验&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#12-把最大堆移植到c&quot; id=&quot;markdown-toc-12-把最大堆移植到c&quot;&gt;12. 把最大堆移植到c++&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#13-优化_updated_affected_word_count&quot; id=&quot;markdown-toc-13-优化_updated_affected_word_count&quot;&gt;13. 优化_updated_affected_word_count&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#14-把上面的优化移植到c&quot; id=&quot;markdown-toc-14-把上面的优化移植到c&quot;&gt;14. 把上面的优化移植到c++&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#本系列全部文章&quot; id=&quot;markdown-toc-本系列全部文章&quot;&gt;本系列全部文章&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;1-问题分析&quot;&gt;1. 问题分析&lt;/h2&gt;

&lt;p&gt;在上面我们的bpe_v5_time中,update time只有500多秒，但是max的时间是30000多秒。怎么才能优化这个时间呢？之前我们尝试的办法是用并行算法求max，Python的版本并不成功，因为Python的GIL使得多线程无法同时使用多个CPU，而多进程又存在大量的通信。我们又尝试了在c++里用OpenMP实现了并行，在32线程的时候max的时间降低到400秒。接着我们又用更快的flat hashmap替代了std::unordered_map，使得max的时间在不用并行算法就能把max时间降低到100秒之内。&lt;/p&gt;

&lt;p&gt;不过我们还是回到Python，除了优化数据结构，我们还有没有办法能够优化算法呢？优化数据结构就是让同样的工作做得更快，而优化算法是用更少的工作达到同样的目的。当然这两者也不是完全分开的，有的时候为了实现某种不同的算法就需要设计合适的数据结构来完成。&lt;/p&gt;

&lt;p&gt;为了找到一个集合中的最大值，我们当然需要遍历整个集合，这个时间不可能更少。也就是说第一次遍历整个集合的时间是不可能变少的。但是第二次遍历时我们就有可能优化了，因为两次遍历之间只有部分pair的计数是发生变化的。刚开始都是高频词，因此affected_words很多，所以受到影响的pair相对较多。而到了后面，词频没有那么高了，affected_words就少了。最简单的想法就是我们可以给pair_counts排序，然后某些pair的计数发生了变化，那么就可以只对这些pair重新排序。因为我们的词频发生变化只有两种可能：老的pair词频减少，新增的pair词频增加。所以我们还可以用一些启发式规则，比如某个pair当前的序号是大于32000(我们最终需要的词表大小)，并且它的频次变低了，那么我们可以忽略它。&lt;/p&gt;

&lt;p&gt;不过我们还有更好的方法来找到一个集合的最大值，那就是使用堆这种数据结构。&lt;/p&gt;

&lt;h2 id=&quot;2-堆heap&quot;&gt;2. 堆(heap)&lt;/h2&gt;

&lt;p&gt;在计算机领域，堆有两个完全不同的意思：一是表示一种数据结构；二是一种内存分配方式。有趣的是在表示内存分配方式时和堆对应的是栈(stack)，而栈除了表示内存分配方式之外也表示一种数据结构。不过作为数据结构的栈和内存分配的栈还是存在紧密联系的，比如数据结构的栈是一种后进先出的数据结构(和先入先出的队列对应)，而内存分配的栈(函数)正是利用了这种后进先出的特点，使得函数的调用和返回顺序符合我们的期望。但是数据结构的堆和内存分配的堆就没有什么联系了，它们唯一的联系就是当初起名字的人不知怎的偶然用了相同的词表示了完全不同两个概念。&lt;/p&gt;

&lt;p&gt;我们这里关注的是作为数据结构的堆，它通常用于实现优先队列，也可以用来实现堆排序。我这里不再详细介绍堆，如果不熟悉的读者随便找一本数据结构和算法的书，或者上网搜索都有很多介绍，比如&lt;a href=&quot;https://en.wikipedia.org/wiki/Heap_(data_structure)&quot;&gt;wiki&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&quot;3-使用堆求max&quot;&gt;3. 使用堆求max&lt;/h2&gt;

&lt;p&gt;一般使用堆的时候我们只是使用如下3个操作：把一个数组变成一个堆(heapify)、从堆顶弹出一个元素(heappop)和把一个元素加入堆(heappush)。我们首先需要通过heapify把一个数组变成堆(满足堆的定义：树根比它的子树中的每一个都大/小)，然后不断的调用heappop/heappush，这两个操作完成之后的数组依然还是堆。&lt;/p&gt;

&lt;p&gt;这里有一个问题，那就是每次找完最大之后(heappop)，我们会合并pair，这会使得某些老的pair的计数减少，同时一些新的pair出现(原来没有)。增加新的pair没有问题，我们只需要调用heappush就可以了。但是怎么修改老的pair的计数并且使得修改之后还是堆呢？我们来看一个例子：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         11
       /    \
      /      \
     9        4
   /   \     / \
  7     8   3   1 
 / \   / \
6  4  5   7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;如果我们把8改成10，也就是一个元素变大了，那么需要从8开始往上调用siftup：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         11
       /    \
      /      \
     10       4
   /   \     / \
  7     9   3   1 
 / \   / \
6  4  5   7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;而反正如果我们把8改成了6，那么需要从8开始往下调用siftdown：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         11
       /    \
      /      \
     10       4
   /   \     / \
  7     7   3   1 
 / \   / \
6  4  5   6
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;但是这里有一个问题，那就是我们怎么找到这个修改的元素。请读者回顾一下，我们的主要数据是pair_counts，这是一个dict，但是堆需要操作的是一个数组(list)，因此我们需要把pair_counts中的元素复制一份到pair_heap这个list里。但是list无法支持快速查找，如果为了修改某个元素就需要顺序扫描一遍list，那就得不偿失了(max都求出来了)。&lt;/p&gt;

&lt;p&gt;一种解决办法是在pair_counts里记录它在list里的下标，这样pair_counts变成：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pair -&amp;gt; (count, index_in_pair_heap)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;那么新增一个pair到pair_counts的同时也把这pair通过heappush添加到pair_heap的合适位置，这就要求heappush函数不但可以把一个元素添加到pair_heap数组，而且还需要返回它在pair_heap的下标。这样我们才能把这个下标保存到pair_counts里。等到后面某个pair的计数发生改变(我们这里只会减少)，我们就可以通过pair_counts找到它在pair_heap的位置，然后对这个位置的元素调用siftdown。&lt;/p&gt;

&lt;p&gt;这就需要修改heappush，而且还需要维护pair_counts和pair_heap的关系，这会使得代码变得很复杂。感兴趣的读者可以尝试实现一下这种算法。&lt;/p&gt;

&lt;p&gt;不过我这里使用的是另外一种方法——lazy的修改。这种方法当某个pair的计数发生改变时，我们什么也不做。只有当我们调用heappop时需要检查一下它的计数是否修改了，这可以通过和pair_counts里的对比来发现。如果修改了(只会变小，这个假设非常重要)，那么我们把新的计数重新通过heappush加进去。然后不断的heappop，直到某个元素没有被修改，那么它就是当前的最大值。&lt;/p&gt;

&lt;p&gt;说起来比较复杂，我通过一个例子来看。比如当前的堆为：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         11
       /    \
      /      \
     9        4
   /   \     / \
  7     8   3   1 
 / \   / \
6  4  5   7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;最大的应该是11，但是我们假设因为合并11变成了8，那么当我们弹出11这个pair时，我们查询pair_counts得知它的最新计数是10，因为它变小了，所以我们不能确定它是否最大。所以我们首先把11弹出，得到：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;          9
       /    \
      /      \
     8        4
   /   \     / \
  7     7   3   1 
 / \   /
6  4  5 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后还需要把10重新heappush进去，得到：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         10
       /    \
      /      \
     9        4
   /   \     / \
  7     8   3   1 
 / \   /  \
6  4  5    7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接着，我们再次弹出当前最大10，这个时候查询pair_counts得知它的计数是最新的，因此就找到了当前的最大pair是10。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：我们可以lazy更新的一个重要假设是老pair的计数只会变小。如果这个假设不成立，比如当前我们的堆是：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;         11
       /    \
      /      \
     9        4
   /   \     / \
  7     8   3   1 
 / \   / \
6  4  5   7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们把1变成了12，那么我们就必须马上对1进行siftup，否则我们找到的最大是11，但这是不正确的。&lt;/p&gt;

&lt;p&gt;使用这种算法，我们不需要在pair_counts里维护pair_heap的下标，而且我们也不需要调用siftdown，这在Python的heapq里是一个私有函数_siftdown，使用私有函数是有风险的，也许新的版本就没有这个函数了。&lt;/p&gt;

&lt;h2 id=&quot;4-python的heapq模块&quot;&gt;4. Python的heapq模块&lt;/h2&gt;

&lt;p&gt;Python的标准库提供了&lt;a href=&quot;https://docs.python.org/3/library/heapq.html&quot;&gt;heapq&lt;/a&gt;。我们需要用到的主要函数是heappush、heappop和heapify。不熟悉的读者可以参考&lt;a href=&quot;https://realpython.com/python-heapq-module/&quot;&gt;The Python heapq Module: Using Heaps and Priority Queues&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;不过这里有一个问题，我们需要的是一个大堆(max heap)，但是Python的heapq模块提供的是小堆(min heap)。我们后面分析其代码时会发现它的内部已经实现了大堆。不过这里我们需要讨论如果只有小堆我们能不能用它来实现大堆的功能。&lt;/p&gt;

&lt;p&gt;一种常见的技巧是把元素逆转来实现大堆。比如如果堆的元素是正整数，那么我们可以存入它对应的负整数，这样就可以得到大堆了，比如下面的例子：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;import heapq
arr = [3, 5, 1, 2, 6, 8, 7]

arr2 = [-i for i in arr]

arr2
[-3, -5, -1, -2, -6, -8, -7]

heapq.heapify(arr2)
arr2
[-8, -6, -7, -2, -5, -1, -3]

arr3 = [-i for i in arr2]
arr3
[8, 6, 7, 2, 5, 1, 3]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;但是现在我们需要放入堆的是(count, pair_string[pair], pair)。如果count大，那么这个tuple就大；如果count相同，我们再比较这个pair的字符串，选择大的那个。tuple最后一个放入pair是为了使用方便。&lt;/p&gt;

&lt;p&gt;如果按照上面的办法，count可以放入-count。pair_strings[pair]呢？pair_strings[pair]里是个tuple，这个tuple的每个元素都是bytes。如果是定长的bytes，因为一个byte的范围是0~255，那么我们可以用255减去这个数来逆转它。比如：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;b1 = b&apos;us&apos;

b2 = b&apos;ua&apos;

b1 &amp;gt; b2
True

c1 = bytes([255 - b for b in b1])
c1
b&apos;\x8a\x8c&apos;

c2 = bytes([255 - b for b in b2])

c1 &amp;lt; c2
True
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;但是如果字符串不定长，就会出问题，比如：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;b1 = b&apos;us&apos;

b2 = b&apos;usb&apos;

b1 &amp;lt; b2
True

c1 = bytes([255 - b for b in b1])

c2 = bytes([255 - b for b in b2])

c1 &amp;gt; c2
False
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;b1比b2少一个字符串。用255减去逆转之后前面的b’us’是相同的，但是不管是否逆转，位数少的都是小于位数多的。&lt;/p&gt;

&lt;p&gt;所以我们需要一个大堆。&lt;/p&gt;

&lt;h2 id=&quot;5-实现一个大堆&quot;&gt;5. 实现一个大堆&lt;/h2&gt;

&lt;p&gt;我们只需要稍微修改一下Python自带的heapq模块就可以把它改成大堆，所以我们把heapq的源代码复制一遍然后修改成&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/maxheap_py.py&quot;&gt;maxheap_py.py&lt;/a&gt;。heapq的函数很多，我们只需要保留heappush、heappop和heapify。另外这3个函数又依赖_siftdown(我把它改名为_siftdown_max)和_siftup(改名_siftup_max)。&lt;/p&gt;

&lt;p&gt;完整代码我就不展开，这里就对比一下_siftdown_max和_siftdown：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;def _siftdown_max(heap, startpos, pos):
    &apos;Maxheap variant of _siftdown&apos;
    newitem = heap[pos]
    # Follow the path to the root, moving parents down until finding a place
    # newitem fits.
    while pos &amp;gt; startpos:
        parentpos = (pos - 1) &amp;gt;&amp;gt; 1
        parent = heap[parentpos]
        if parent &amp;lt; newitem:
            heap[pos] = parent
            pos = parentpos
            continue
        break
    heap[pos] = newitem
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;def _siftdown(heap, startpos, pos):
    newitem = heap[pos]
    # Follow the path to the root, moving parents down until finding a place
    # newitem fits.
    while pos &amp;gt; startpos:
        parentpos = (pos - 1) &amp;gt;&amp;gt; 1
        parent = heap[parentpos]
        if newitem &amp;lt; parent:
            heap[pos] = parent
            pos = parentpos
            continue
        break
    heap[pos] = newitem
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;它们唯一的区别就是”if newitem &amp;lt; parent”和”if parent &amp;lt; newitem”这两行代码。这里假设newitem和parent值实现/重载了小于运算符，所以即使想求较大的元素也是通过调换顺序通过小于函数来实现的。&lt;/p&gt;

&lt;h2 id=&quot;6-用c模块优化maxheap_py&quot;&gt;6. 用c模块优化maxheap_py&lt;/h2&gt;

&lt;p&gt;对于c模块开发不感兴趣的读者可以跳过本节内容。&lt;/p&gt;

&lt;p&gt;但是如果我们比较自己版本速度，它会把CPython的版本慢很多，原因在于CPython的版本内部是调用了对应的c模块。如果我们仔细阅读&lt;a href=&quot;https://github.com/python/cpython/blob/3.12/Lib/heapq.py&quot;&gt;heap源代码&lt;/a&gt;，我们会发现：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# If available, use C implementation
try:
    from _heapq import *
except ImportError:
    pass
try:
    from _heapq import _heapreplace_max
except ImportError:
    pass
try:
    from _heapq import _heapify_max
except ImportError:
    pass
try:
    from _heapq import _heappop_max
except ImportError:
    pass
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;也就是它会(尝试)调用对应的c模块实现。具体的代码在&lt;a href=&quot;https://github.com/python/cpython/blob/3.12/Modules/_heapqmodule.c&quot;&gt;_heapqmodule.c&lt;/a&gt;里。&lt;/p&gt;

&lt;p&gt;我复制它实现了大堆的功能，完整代码在&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/_maxheapqmodule.c&quot;&gt;_maxheapqmodule.c&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;我们只看其中一个函数siftdown：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;static int
siftdown(PyListObject *heap, Py_ssize_t startpos, Py_ssize_t pos)
{
    PyObject *newitem, *parent, **arr;
    Py_ssize_t parentpos, size;
    int cmp;

    assert(PyList_Check(heap));
    size = PyList_GET_SIZE(heap);
    if (pos &amp;gt;= size) {
        PyErr_SetString(PyExc_IndexError, &quot;index out of range&quot;);
        return -1;
    }

    /* Follow the path to the root, moving parents down until finding
       a place newitem fits. */
    arr = _PyList_ITEMS(heap);
    newitem = arr[pos];
    while (pos &amp;gt; startpos) {
        parentpos = (pos - 1) &amp;gt;&amp;gt; 1;
        parent = arr[parentpos];
        Py_INCREF(newitem);
        Py_INCREF(parent);
        cmp = PyObject_RichCompareBool(newitem, parent, Py_GT);
        Py_DECREF(parent);
        Py_DECREF(newitem);
        if (cmp &amp;lt; 0)
            return -1;
        if (size != PyList_GET_SIZE(heap)) {
            PyErr_SetString(PyExc_RuntimeError,
                            &quot;list changed size during iteration&quot;);
            return -1;
        }
        if (cmp == 0)
            break;
        arr = _PyList_ITEMS(heap);
        parent = arr[parentpos];
        newitem = arr[pos];
        arr[parentpos] = newitem;
        arr[pos] = parent;
        pos = parentpos;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;两种对比代码只有一行区别：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cmp = PyObject_RichCompareBool(newitem, parent, Py_GT);
cmp = PyObject_RichCompareBool(newitem, parent, Py_LT);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;前面介绍过PyObject_RichCompareBool，它就是根据第三个参数比较前两个参数。如果返回值小于0，表示异常；如果返回值大于0，则表示第三个参数，第一个比第二个大/小是true；如果返回值是0，则表示第一个参数比第二个参数大/小是false。&lt;/p&gt;

&lt;p&gt;因此我们把小堆改成大堆只需要把比较运算符从Py_LT(小于)变成Py_GT(大于)就行了。&lt;/p&gt;

&lt;p&gt;当然，其它还有一些细节需要修改，比如我把模块名字改成了maxheapqc，那么就需要修改相应的代码：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;static struct PyModuleDef _heapqmodule = {
    PyModuleDef_HEAD_INIT,
    &quot;maxheapqc&quot;,
    module_doc,
    0,
    heapq_methods,
    heapq_slots,
    NULL,
    NULL,
    NULL
};

PyMODINIT_FUNC
PyInit_maxheapqc(void)
{
    return PyModuleDef_Init(&amp;amp;_heapqmodule);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;另外为了编译这个C模块，需要修改setup.py：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;include_path = sysconfig.get_path(&apos;include&apos;)
internal_include_path = os.path.join(include_path, &apos;internal&apos;)

print(f&quot;{internal_include_path=}&quot;)
project_root = os.path.dirname(os.path.abspath(__file__))

maxheapqc_module = Extension(&apos;cs336_basics.maxheapqc&apos;, sources=[&apos;cs336_basics/_maxheapqmodule.c&apos;],
        extra_compile_args=[
            f&apos;-I{internal_include_path}&apos;,
        ]
        )

setup(
    name=&apos;maxheapqc&apos;,
    version=&apos;1.0&apos;,
    description=&apos;maxheapqc&apos;,
    packages=[&apos;cs336_basics&apos;],
    ext_modules=[maxheapqc_module]
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们在编译_maxheapqmodule.c时，需要依赖：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#include &quot;Python.h&quot;
#include &quot;pycore_list.h&quot; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;对于Python.h，setuptools会帮我们包含这个头文件，它的位置通常类似于：/usr/local/include/python3.12/Python.h。但是我们的_maxheapqmodule.c还需要通过c接口操作list，这需要pycore_list.h，它的位置在类似于/usr/local/include/python3.12/internal/pycore_list.h的地方。我们不想硬编码这个位置，而且同一个系统里可能安装了很多版本的python，还有很多conda这样的环境。所以我们可以通过sysconfig.get_path(‘include’)得到当前Python解释器的路径，然后再在里面找internal。&lt;/p&gt;

&lt;p&gt;我们在编译时需要通过-I添加这个路径：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;maxheapqc_module = Extension(&apos;cs336_basics.maxheapqc&apos;, sources=[&apos;cs336_basics/_maxheapqmodule.c&apos;],
        extra_compile_args=[
            f&apos;-I{internal_include_path}&apos;,
        ]
        )
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个-I参数好像是gcc添加头文件的方式，我不知道在非linux系统非gcc编译器下能否通用，如果读者使用的是其它编译器，请参考编译器的手册添加合适的头文件路径。&lt;/p&gt;

&lt;p&gt;运行下面的命令编译c模块：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python setup.py build_ext -i
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;编译后可以得到类似cs336_basics/maxheapqc.cpython-312-x86_64-linux-gnu.so。接下来我们就可以写一个&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/maxheapq.py&quot;&gt;maxheapq.py&lt;/a&gt;来调用它了。&lt;/p&gt;

&lt;p&gt;maxheapq.py的代码和heapq.py类似，它用Python实现了大堆的功能，并且会尝试加载maxheapqc：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# If available, use C implementation
try:
    from cs336_basics.maxheapqc import *
    print(&quot;load c!&quot;)
except ImportError:
    pass
try:
    from cs336_basics.maxheapqc import _heapreplace_max
except ImportError:
    pass
try:
    from cs336_basics.maxheapqc import _heapify_max
except ImportError:
    pass
try:
    from cs336_basics.maxheapqc import _heappop_max
except ImportError:
    pass
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;7-用heapq实现大堆&quot;&gt;7. 用heapq实现大堆&lt;/h2&gt;

&lt;p&gt;其实如果我们阅读了heapq的代码，我们会发现它已经实现了_heapify_max、_siftdown_max和_siftup_max。而其中_heapify_max是有对应的c模块代码，速度比较快。但是_siftdown_max和_siftup_max还是用Python实现的。我们可以查看&lt;a href=&quot;https://github.com/python/cpython/blob/3.12/Modules/_heapqmodule.c#L540&quot;&gt;这里&lt;/a&gt;来找到哪些函数有c语言实现：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;static PyMethodDef heapq_methods[] = {
    _HEAPQ_HEAPPUSH_METHODDEF
    _HEAPQ_HEAPPUSHPOP_METHODDEF
    _HEAPQ_HEAPPOP_METHODDEF
    _HEAPQ_HEAPREPLACE_METHODDEF
    _HEAPQ_HEAPIFY_METHODDEF
    _HEAPQ__HEAPPOP_MAX_METHODDEF
    _HEAPQ__HEAPIFY_MAX_METHODDEF
    _HEAPQ__HEAPREPLACE_MAX_METHODDEF
    {NULL, NULL}           /* sentinel */
};
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们可以利用heapq的这3个函数来实现大堆。代码在&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/maxheap_heapq.py&quot;&gt;maxheap_heapq.py&lt;/a&gt;。我们来看一下：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;def heappush(heap, item):
    heap.append(item)
    heapq._siftdown_max(heap, 0, len(heap)-1)


def heappop(heap):
    &quot;&quot;&quot;Maxheap version of a heappop.&quot;&quot;&quot;
    lastelt = heap.pop()  # raises appropriate IndexError if heap is empty
    if heap:
        returnitem = heap[0]
        heap[0] = lastelt
        heapq._siftup_max(heap, 0)
        return returnitem
    return lastelt

def heapify(heap):
    heapq._heapify_max(heap)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里heapify会调用c接口，速度比较快，但是heappush和heappop还是Python实现。&lt;/p&gt;

&lt;h2 id=&quot;8-不同的大堆实现的性能测试&quot;&gt;8. 不同的大堆实现的性能测试&lt;/h2&gt;

&lt;p&gt;我写了一个简单的测试代码&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/test_heap_speed.py&quot;&gt;test_heap_speed.py&lt;/a&gt;。结果如下：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;maxheapq_py: 40.50480842590332
maxheap_heapq: 4.577162265777588
maxheapq: 4.334884405136108
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里主要的时间用在heapify，maxheapq_py是Python实现，所以比较慢。&lt;/p&gt;

&lt;p&gt;而&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/test_heap_speed2.py&quot;&gt;test_heap_speed2.py&lt;/a&gt;主要测试heappush和heappop，结果如下：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;maxheapq_py: 1.892250157892704
maxheap_heapq: 1.8967562650796026
maxheapq: 0.3524886401137337
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;可以看到maxheapq要比Python的实现快。&lt;/p&gt;

&lt;h2 id=&quot;9-用大堆求max&quot;&gt;9. 用大堆求max&lt;/h2&gt;

&lt;p&gt;完整的代码在&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v6.py&quot;&gt;bpe_v6.py&lt;/a&gt;。我们只看一下它和bep_v5.py的区别。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;from cs336_basics import maxheap_py as maxheap

    def train(self, input_path, vocab_size, special_tokens, *args):
        ...
        pair_heap = []
        for pair, count in pair_counts.items():
            maxheap.heappush(pair_heap, (count, pair_strings[pair], pair))
            
            
            

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;首先需要import maxheap_py，为了便于切换不同的大堆实现，我把它重命名为maxheap。因为所有不同大堆的接口都相同，所以要切换为maxheapq或者maxheap_heapq只需要修改这一行代码就可以了。&lt;/p&gt;

&lt;p&gt;然后在第一次统计了pair_counts之后，我们需要构造一个pair_heap，这里是不停调用maxheap.heappush。另外一种实现方法是使用heapify：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;        pair_heap = []
        for pair, count in pair_counts.items():
            pair_heap.append((count, pair_strings[pair], pair))
        maxheap.heapify(pair_heap)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;两种方法最终都会构建出一个堆，但是它们的结果可能不完全相同。理论上用heapify的方法更快一点，不过实际测试发现它们的时间差很小，因为pair_counts一开始不到两万。&lt;/p&gt;

&lt;p&gt;接下来是主要的修改之处，通过heappop求当前最大的pair：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    @staticmethod
    def _merge_a_pair(pair_counts, pair_strings, vocabulary, pair_to_words, 
                   word_counts, word_encodings, merges, size, pair_heap):
        
        while pair_heap:
            count, string_priority, merge_pair = maxheap.heappop(pair_heap)
            
            # check pair validity
            if merge_pair in pair_counts and pair_counts[merge_pair] == count:
                break
            elif merge_pair in pair_counts:
                # update count (lazily)
                maxheap.heappush(pair_heap, (pair_counts[merge_pair], 
                                               string_priority, 
                                               merge_pair))
        else:
            # no valid pairs found
            return False
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个算法的实现和前面描述的一样：首先从堆顶弹出当前最大的merge_pair，然后通过对比pair_counts[merge_pair]来检查它的count有没有更新，如果没有更新，那么merge_pair就是当前的最大pair，break出循环。如果有了更新而且新的count&amp;gt;0(merge_pair in pair_counts)，则需要把新的count重新插入到堆里。然后再循环去找最大的pair。&lt;/p&gt;

&lt;p&gt;最后一处修改就是每次merge产生新的pair时，我们需要把它也加到堆里：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    @staticmethod
    def _updated_affected_word_count(merge_pair, affected_words, word_encodings, 
                                     word_counts, pair_counts, pair_to_words, 
                                     new_id, pair_strings, vocabulary, pair_heap):



        for new_pair in new_pairs:
            if new_pair not in pair_strings:
                pair_strings[new_pair] = (vocabulary[new_pair[0]], vocabulary[new_pair[1]])

            maxheap.heappush(pair_heap, (pair_counts[new_pair], pair_strings[new_pair], new_pair))
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;10-测试&quot;&gt;10. 测试&lt;/h2&gt;

&lt;p&gt;为了测试时间，我也实现了&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v6_time.py&quot;&gt;bpe_v6_time.py&lt;/a&gt;。测试结果如下：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;数据&lt;/td&gt;
      &lt;td&gt;总时间(s)&lt;/td&gt;
      &lt;td&gt;统计词频时间(s)&lt;/td&gt;
      &lt;td&gt;合并时间(s)&lt;/td&gt;
      &lt;td&gt;其它&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v5_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;34333/34853/35804&lt;/td&gt;
      &lt;td&gt;401/390/401&lt;/td&gt;
      &lt;td&gt;total:33879/34401/35347 max:33353/33820/34816 update:525/579/530&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v6_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1036/1107/1046&lt;/td&gt;
      &lt;td&gt;395/395/398&lt;/td&gt;
      &lt;td&gt;total: 576/641/591 max:6/7/6 update: 570/633/584&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;使用堆来求最大之后，合并时间从30000多秒减少到了570多秒。heappush的时间是100多秒，我们还能通过其它更快的大堆来优化它吗？&lt;/p&gt;

&lt;p&gt;把maxheap_py改成maxheap_heapq或者maxheapq就得到&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v7.py&quot;&gt;bpe_v7.py&lt;/a&gt;和&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v7_maxheapc.py&quot;&gt;bpe_v7_maxheapc.py&lt;/a&gt;，测试结果如下：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;数据&lt;/td&gt;
      &lt;td&gt;总时间(s)&lt;/td&gt;
      &lt;td&gt;统计词频时间(s)&lt;/td&gt;
      &lt;td&gt;合并时间(s)&lt;/td&gt;
      &lt;td&gt;其它&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v5_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;34333/34853/35804&lt;/td&gt;
      &lt;td&gt;401/390/401&lt;/td&gt;
      &lt;td&gt;total:33879/34401/35347 max:33353/33820/34816 update:525/579/530&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v6_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1036/1107/1046&lt;/td&gt;
      &lt;td&gt;395/395/398&lt;/td&gt;
      &lt;td&gt;total: 576/641/591 max:6/7/6 update: 570/633/584&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1062/1035/1036&lt;/td&gt;
      &lt;td&gt;392/397/395&lt;/td&gt;
      &lt;td&gt;total: 606/573/577 max:6/6/6 update: 599/567/571&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_maxheapc_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1069/1017/1011&lt;/td&gt;
      &lt;td&gt;400/401/399&lt;/td&gt;
      &lt;td&gt;total: 606/556/555 max: 3/3/3 update: 602/552/552&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;使用更快的堆实现并没有加快速度。我猜测是因为heappush的时间占总update时间的比例比较低，因此它的微小时间变化对于整体影响不大。&lt;/p&gt;

&lt;h2 id=&quot;11-更大vocab_size的实验&quot;&gt;11. 更大vocab_size的实验&lt;/h2&gt;

&lt;p&gt;如果我们拿bpe_v7和c++版本的bpe_train_updater_fine_grained_emhash8相比：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;数据&lt;/td&gt;
      &lt;td&gt;总时间(s)&lt;/td&gt;
      &lt;td&gt;统计词频时间(s)&lt;/td&gt;
      &lt;td&gt;合并时间(s)&lt;/td&gt;
      &lt;td&gt;其它&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1028/1053/1045&lt;/td&gt;
      &lt;td&gt;395/397/393&lt;/td&gt;
      &lt;td&gt;total: 575/589/590 max: 6/6/6 update: 569/583/583 make heap: 0.01/0.01 heap_push_time: 102/107/122&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program&lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;bpe_v7的merge总时间是600秒左右，而bpe_train_updater_fine_grained_emhash8的总时间是260秒左右。但是如果我们把合并的次数增多呢？因为现在的大模型的词典越来越大，所以我们测试一下vocab_size是64000和96000的结果：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;数据&lt;/td&gt;
      &lt;td&gt;总时间(s)&lt;/td&gt;
      &lt;td&gt;统计词频时间(s)&lt;/td&gt;
      &lt;td&gt;合并时间(s)&lt;/td&gt;
      &lt;td&gt;其它&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1028/1053/1045&lt;/td&gt;
      &lt;td&gt;395/397/393&lt;/td&gt;
      &lt;td&gt;total: 575/589/590 max: 6/6/6 update: 569/583/583 make heap: 0.01/0.01 heap_push_time: 102/107/122&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time2&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1111/1174/1100&lt;/td&gt;
      &lt;td&gt;393/420/403&lt;/td&gt;
      &lt;td&gt;total: 655/686/639 max: 9/10/10 update: 645/675/628 make heap: 0.01/0.01/0.01 heap_push_time: 128/157/108&lt;/td&gt;
      &lt;td&gt;vocab_size=64000&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time2&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1129/1130/1123&lt;/td&gt;
      &lt;td&gt;394/406/393&lt;/td&gt;
      &lt;td&gt;total: 675/666/670 max: 13/12/12 update: 661/653/657 make heap: 0.01/0.01/0.01 heap_push_time: 143/152/120&lt;/td&gt;
      &lt;td&gt;vocab_size=96000&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;可以看到由于后面处理的都是低频词和低频的pair，所以几乎时间没有什么变化。我们再看bpe_train_updater_fine_grained_emhash8：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program&lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;413/414/406&lt;/td&gt;
      &lt;td&gt;233/227/223&lt;/td&gt;
      &lt;td&gt;179/187/182&lt;/td&gt;
      &lt;td&gt;64k&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;664/593/606&lt;/td&gt;
      &lt;td&gt;305/255/269&lt;/td&gt;
      &lt;td&gt;358/338/337&lt;/td&gt;
      &lt;td&gt;96k&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;可以看到bpe_train_updater_fine_grained_emhash8的时间逐渐增大，这是因为随着pair_counts中pair的增多，max的时间只会增加不会减少。&lt;/p&gt;

&lt;h2 id=&quot;12-把最大堆移植到c&quot;&gt;12. 把最大堆移植到c++&lt;/h2&gt;

&lt;p&gt;我们也可以把这个算法移植到c++，c++的标准库的&lt;algorithm&gt;里有std::make_heap、std::push_heap和std::pop_heap等函数，可以实现和python相同的功能，而且c++的堆是大堆，我们只需要实现operator&amp;lt;的重载即可，我们可以定义：&lt;/algorithm&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;struct HeapItem{
    int count;
    std::vector&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt; pair_string;
    std::pair&amp;lt;int,int&amp;gt; pair;

    bool operator&amp;lt;(const HeapItem&amp;amp; other) const;
};
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;标准库的用法可以参考&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/test_max_heap.cpp&quot;&gt;test_max_heap.cpp&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;我这里没有使用标准库，而是把Python版本的用c++重写了，测试了一下速度，似乎比标准库的还要稍微快一点点。大堆的完整代码在&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/max_heap.cpp&quot;&gt;max_heap.cpp&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;基于bpe_train_updater_fine_grained实现了&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap.cpp&quot;&gt;bpe_train_updater_fine_grained_heap.cpp&lt;/a&gt;，基于bpe_train_updater_fine_grained_emhash8_set实现了&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_emhash8_set.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set.cpp&lt;/a&gt;，基于bpe_train_updater_fine_grained_emhash8_set9实现了&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_emhash8_set9.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9.cpp&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;代码实现逻辑和Python版本完全一致，感兴趣的读者可以自行阅读。下面是对比实验结果：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program&lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8_set&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;192/192/194&lt;/td&gt;
      &lt;td&gt;117/117/117&lt;/td&gt;
      &lt;td&gt;75/75/77&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8_set9&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;168/170/171&lt;/td&gt;
      &lt;td&gt;107/108/109&lt;/td&gt;
      &lt;td&gt;61/62/61&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;200/228/211&lt;/td&gt;
      &lt;td&gt;194/220/208&lt;/td&gt;
      &lt;td&gt;2/3/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;199/200/210&lt;/td&gt;
      &lt;td&gt;193/195/203&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;139/122/128&lt;/td&gt;
      &lt;td&gt;136/118/123&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set9&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;111/121/122&lt;/td&gt;
      &lt;td&gt;109/116/116&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;可以看到，使用堆来求max的时间不到2秒。我们再来看一下它在合并词典64000和96000时的结果：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program&lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;413/414/406&lt;/td&gt;
      &lt;td&gt;233/227/223&lt;/td&gt;
      &lt;td&gt;179/187/182&lt;/td&gt;
      &lt;td&gt;64k&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;664/593/606&lt;/td&gt;
      &lt;td&gt;305/255/269&lt;/td&gt;
      &lt;td&gt;358/338/337&lt;/td&gt;
      &lt;td&gt;96k&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;199/200/210&lt;/td&gt;
      &lt;td&gt;193/195/203&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;224/228/227&lt;/td&gt;
      &lt;td&gt;216/218/217&lt;/td&gt;
      &lt;td&gt;3/3/3&lt;/td&gt;
      &lt;td&gt;64k&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;249/255/240&lt;/td&gt;
      &lt;td&gt;239/238/230&lt;/td&gt;
      &lt;td&gt;5/6/4&lt;/td&gt;
      &lt;td&gt;96k&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;通过对比我们可以发现，使用堆的bpe_train_updater_fine_grained_heap_emhash8算法词典从32k增加到64k时update时间值增加了20秒左右，max时间几乎没变；而bpe_train_updater_fine_grained_emhash8的max时间增加了一倍。&lt;/p&gt;

&lt;h2 id=&quot;13-优化_updated_affected_word_count&quot;&gt;13. 优化_updated_affected_word_count&lt;/h2&gt;

&lt;p&gt;如果仔细阅读_updated_affected_word_count函数和fine_grained_pair_counter_diff，可以发现其中new_pairs和diff_pairs存在冗余。回顾一下，diff_pairs记录了pair的count的变化，如果大于零表示增加(新pair)；如果小于零表示减少(老pair)。而new_pairs表示受到影响的所有pair(还包括可能没有变化的pair，这种情况发生在merge_pair出现多次时夹在中间的那些没有变化的pair)。但是根据之前的算法，我们只需要把新pair通过heappush加入堆就行，老的pair可以lazy的方式更新。所以我们可以去掉new_pairs，而diff_pairs里count&amp;gt;0的pair就一定是新pair。&lt;/p&gt;

&lt;p&gt;这样就得到&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v7_maxheapc_opt_time.py&quot;&gt;bpe_v7_maxheapc_opt_time.py&lt;/a&gt;，其它版本也可以参考这个修改。&lt;/p&gt;

&lt;p&gt;主要改动的代码是：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    @staticmethod
    def _updated_affected_word_count(merge_pair, affected_words, word_encodings, 
                                     word_counts, pair_counts, pair_to_words, 
                                     new_id, pair_strings, vocabulary, pair_heap):
        # we may update/delete words when iterate it.
        affected_words = affected_words.copy()
        diff_pairs = defaultdict(int)

        BPE_Trainer.fine_grained_pair_counter_diff(affected_words, word_encodings, word_counts, merge_pair, diff_pairs, 
                             new_id, pair_to_words)
        for pair, count in diff_pairs.items():
            if count == 0: continue
            pair_counts[pair] += count
            if count &amp;gt; 0: # new pair
                pair_strings[pair] = (vocabulary[pair[0]], vocabulary[pair[1]])
                maxheap.heappush(pair_heap, (pair_counts[pair], pair_strings[pair], pair))
            
            if pair_counts[pair] &amp;lt;= 0: # should not less than 0!
                del pair_counts[pair]
                pair_to_words.pop(pair, None)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;BPE_Trainer.fine_grained_pair_counter_diff不再需要传入new_pairs这个出参。在遍历diff_pairs时我们判断一下count，如果count&amp;gt;0则说明是一个新加入的pair，我们保存它的pair_strings然后通过heappush加入堆中。&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;数据&lt;/td&gt;
      &lt;td&gt;总时间(s)&lt;/td&gt;
      &lt;td&gt;统计词频时间(s)&lt;/td&gt;
      &lt;td&gt;合并时间(s)&lt;/td&gt;
      &lt;td&gt;其它&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v5_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;34333/34853/35804&lt;/td&gt;
      &lt;td&gt;401/390/401&lt;/td&gt;
      &lt;td&gt;total:33879/34401/35347 max:33353/33820/34816 update:525/579/530&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v6_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1036/1107/1046&lt;/td&gt;
      &lt;td&gt;395/395/398&lt;/td&gt;
      &lt;td&gt;total: 576/641/591 max:6/7/6 update: 570/633/584&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1062/1035/1036&lt;/td&gt;
      &lt;td&gt;392/397/395&lt;/td&gt;
      &lt;td&gt;total: 606/573/577 max:6/6/6 update: 599/567/571&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_maxheapc_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;1069/1017/1011&lt;/td&gt;
      &lt;td&gt;400/401/399&lt;/td&gt;
      &lt;td&gt;total: 606/556/555 max: 3/3/3 update: 602/552/552&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v7_maxheapc_opt_time&lt;/td&gt;
      &lt;td&gt;open_web&lt;/td&gt;
      &lt;td&gt;984/965/1000&lt;/td&gt;
      &lt;td&gt;394/394/403&lt;/td&gt;
      &lt;td&gt;total: 532/514/538 max: 0.8/0.8/0.9 update: 531/513/537&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;bpe_v7_maxheapc_opt_time比bpe_v7_maxheapc_time要快7%。&lt;/p&gt;

&lt;h2 id=&quot;14-把上面的优化移植到c&quot;&gt;14. 把上面的优化移植到c++&lt;/h2&gt;

&lt;p&gt;优化后的版本是&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_opt.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_opt.cpp&lt;/a&gt;、&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_emhash8_opt.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_opt.cpp&lt;/a&gt;、&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_emhash8_set_opt.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set_opt.cpp&lt;/a&gt;和&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp&quot;&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt.cpp&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;在OpenWeb数据集上的测试结果为：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program&lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;200/228/211&lt;/td&gt;
      &lt;td&gt;194/220/208&lt;/td&gt;
      &lt;td&gt;2/3/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_opt&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;190/192/190&lt;/td&gt;
      &lt;td&gt;187/189/187&lt;/td&gt;
      &lt;td&gt;0/0/0&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;199/200/210&lt;/td&gt;
      &lt;td&gt;193/195/203&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_opt&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;188/213/192&lt;/td&gt;
      &lt;td&gt;185/209/189&lt;/td&gt;
      &lt;td&gt;0/0/0&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;139/122/128&lt;/td&gt;
      &lt;td&gt;136/118/123&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set_opt&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;110/130/110&lt;/td&gt;
      &lt;td&gt;108/128/108&lt;/td&gt;
      &lt;td&gt;0/0/0&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set9&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;111/121/122&lt;/td&gt;
      &lt;td&gt;109/116/116&lt;/td&gt;
      &lt;td&gt;2/2/2&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;105/102/104&lt;/td&gt;
      &lt;td&gt;102/100/101&lt;/td&gt;
      &lt;td&gt;0/0/0&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;bpe_train_updater_fine_grained_heap_emhash8_set9_opt比bpe_train_updater_fine_grained_heap_emhash8_set9的update时间快11%。bpe_train_updater_fine_grained_heap_emhash8_set_opt比bpe_train_updater_fine_grained_heap_emhash8_set快8%。这个结论和python版本是一致的。&lt;/p&gt;

&lt;h2 id=&quot;本系列全部文章&quot;&gt;本系列全部文章&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/05/bpe-trainer-0/&quot;&gt;第0部分：简介&lt;/a&gt; 介绍bpe训练的基本算法和相关任务，并且介绍开发环境。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/07/bpe-trainer-1/&quot;&gt;第1部分：最简单实现&lt;/a&gt; bpe训练最简单的实现。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/08/bpe-trainer-2/&quot;&gt;第2部分：优化算法&lt;/a&gt; 实现pair_counts的增量更新。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/09/bpe-trainer-3/&quot;&gt;第3部分：并行分词和统计词频&lt;/a&gt; 使用multiprocessing实现多进程并行算法。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/10/bpe-trainer-4/&quot;&gt;第4部分：一次失败的并行优化&lt;/a&gt; 尝试用多进程并行计算max pair。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/12/bpe-trainer-5/&quot;&gt;第5部分：用C++实现Merge算法&lt;/a&gt; 用C++实现和Python等价的merge算法，并且比较std::unordered_map的两种遍历方式。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/15/bpe-trainer-6/&quot;&gt;第6部分：用OpenMP实现并行求最大&lt;/a&gt; 用OpenMP并行求pair_counts里最大pair。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/18/bpe-trainer-7/&quot;&gt;第7部分：使用flat hashmap替代std::unordered_map&lt;/a&gt; 使用flat hashmap来替代std::unordered_map。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/19/bpe-trainer-8/&quot;&gt;第8部分：实现细粒度更新&lt;/a&gt; 使用倒排索引实现pair_counts的细粒度更新算法。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/21/bpe-trainer-9/&quot;&gt;第9部分：使用堆来寻找最大pair&lt;/a&gt; 使用堆来求最大pair，提升性能。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/24/bpe-trainer-10/&quot;&gt;第10部分：使用cython和pypy来加速&lt;/a&gt; 使用cython和pypy来加速python代码。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/25/bpe-trainer-11/&quot;&gt;第11部分：使用cython封装c++代码&lt;/a&gt; 使用cython封装c++代码。&lt;/li&gt;
&lt;/ul&gt;

</description>
        <pubDate>Sun, 21 Sep 2025 00:00:00 +0000</pubDate>
        <link>http://fancyerii.github.io/2025/09/21/bpe-trainer-9/</link>
        <guid isPermaLink="true">http://fancyerii.github.io/2025/09/21/bpe-trainer-9/</guid>
        
        <category>cs336</category>
        
        <category>bpe tokenizer</category>
        
        
      </item>
    
      <item>
        <title>Implementing and Optimizing a BPE Tokenizer from Scratch—Part 8: Implementing Fine-Grained Updates</title>
        <description>&lt;p&gt;This series of articles implements a subtask of Stanford’s CS336 Assignment 1: building an efficient training algorithm for a BPE Tokenizer. Through a series of optimizations, our algorithm’s training time on OpenWebText was reduced from over 10 hours to less than 10 minutes. This series explains these optimizations, including algorithmic improvements, data structure enhancements, parallelization with OpenMP, Cython optimization, and implementing key code in C++ along with its integration via Cython. This is the ninth article, which focuses on optimizing the update process for data structures like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;strong&gt;Table of Content&lt;/strong&gt;&lt;/p&gt;
&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#1-problem-analysis&quot; id=&quot;markdown-toc-1-problem-analysis&quot;&gt;1. Problem Analysis&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-fine-grained-frequency-update-algorithm&quot; id=&quot;markdown-toc-2-fine-grained-frequency-update-algorithm&quot;&gt;2. Fine-Grained Frequency Update Algorithm&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-code-implementation&quot; id=&quot;markdown-toc-3-code-implementation&quot;&gt;3. Code Implementation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-testing&quot; id=&quot;markdown-toc-4-testing&quot;&gt;4. Testing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-porting-to-c&quot; id=&quot;markdown-toc-5-porting-to-c&quot;&gt;5. Porting to C++&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-using-emhash8hashsetemhash9hashset-to-replace-stdunordered_set&quot; id=&quot;markdown-toc-6-using-emhash8hashsetemhash9hashset-to-replace-stdunordered_set&quot;&gt;6. Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashSet&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash9::HashSet&lt;/code&gt; to Replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_set&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#full-series&quot; id=&quot;markdown-toc-full-series&quot;&gt;Full Series&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;1-problem-analysis&quot;&gt;1. Problem Analysis&lt;/h2&gt;

&lt;p&gt;We previously left one optimization point unaddressed: when updating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;, many pairs’ counts don’t actually need to change, but for the sake of simple implementation, we first delete them and then add them back. More specifically, let’s look at a previous example.&lt;/p&gt;

&lt;p&gt;Suppose the word frequency count is:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{low: 5, lower: 2, widest: 3, newest: 6, es: 2, st: 2}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we count the pair frequencies:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{lo: 7, ow: 7, we: 8, er: 2, wi: 3, id: 3, de: 3, es: 11, st: 11, ne: 6, ew: 6}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Based on the merge rules, we choose to merge &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;s&apos;, &apos;t&apos;)&lt;/code&gt;. Since they are merged, we need to update the pair frequency counts. Let’s look at our current update algorithm (the code is in &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v2.py#L55&quot;&gt;BPE_Trainer._updated_affected_word_count&lt;/a&gt;, though a more accurate name would be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_updated_pair_count_of_affected_word&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;To get the new pair frequencies, we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_to_words&lt;/code&gt; to find the words containing the pair &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;s&apos;, &apos;t&apos;)&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&apos;widest&apos;
&apos;newest&apos;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We first subtract all pairs within these two words, which means subtracting:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{wi: 3, id: 3, de: 3, es: 3, st: 3}
{ne: 6, ew: 6, we: 6, es: 6, st: 6}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This gives us:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{lo: 7, ow: 7, we: 2, er: 2, wi: 0, id: 0, de: 0, es: 2, st: 2, ne: 0, ew: 0}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, the tokens for these two words become:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;word_encodings[&apos;widest&apos;] = [&apos;w&apos;,&apos;i&apos;,&apos;d&apos;,&apos;e&apos;,&apos;st&apos;]
word_encodings[&apos;newest&apos;] = [&apos;n&apos;,&apos;e&apos;,&apos;s&apos;,&apos;e&apos;,&apos;st&apos;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Therefore, based on the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;word_encodings&lt;/code&gt;, we calculate the new pair frequencies:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{wi: 3, id: 3, de: 3, est: 3}
{ne: 6, ew: 6, we: 6, est: 6}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Adding these frequencies back gives us:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{lo: 7, ow: 7, we: 8, er: 2, wi: 3, id: 3, de: 3, es: 2, st: 2, ne: 6, ew: 6, est: 9}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As you can see, the frequencies for pairs like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;wi&apos;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;id&apos;&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;de&apos;&lt;/code&gt; were subtracted and then added back, leading to unnecessary updates. Worse still, after &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;wi&apos;&lt;/code&gt; is deleted, its frequency becomes 0, so it needs to be deleted from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_to_words&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                    if pair_counts[old_pair] &amp;lt;= 0:
                        # we accounted for all occurrences of this pair
                        del pair_counts[old_pair]
                        pair_to_words.pop(old_pair)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;According to &lt;a href=&quot;https://www.fluentpython.com/extra/internals-of-sets-and-dicts/&quot;&gt;Internals of sets and dicts&lt;/a&gt;, CPython’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict&lt;/code&gt; uses an open-addressing collision resolution strategy. Deleting a key from a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict&lt;/code&gt; just marks it (unless a rehash occurs), and adding the same key back will reuse the original address. So the impact on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict&lt;/code&gt; itself is minimal, but the key and value objects still need to be reconstructed (since the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict&lt;/code&gt; stores pointers to them). If a separate chaining method like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt; is used, deleting and reinserting will mess up the original order of the linked list (the new element is inserted at the head). For example, if a bucket’s elements were &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a-&amp;gt;b-&amp;gt;c&lt;/code&gt;, deleting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b&lt;/code&gt; and reinserting it might make the new list &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b-&amp;gt;a-&amp;gt;c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, if we analyze carefully, we can identify which deletions are unnecessary. For instance, in our example, we are merging &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;s&apos;, &apos;t&apos;)&lt;/code&gt;. In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;widest&apos;&lt;/code&gt;, the only pairs affected by the merge are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;e&apos;, &apos;s&apos;)&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;s&apos;, &apos;t&apos;)&lt;/code&gt; (there’s no pair after &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;t&apos;&lt;/code&gt;). The pairs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[&apos;w&apos;, &apos;i&apos;]&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[&apos;i&apos;, &apos;d&apos;]&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[&apos;d&apos;, &apos;e&apos;]&lt;/code&gt; are unaffected. Therefore, we only need to remove &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;es&apos;&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;st&apos;&lt;/code&gt; and add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;e&apos;, &apos;st&apos;)&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;2-fine-grained-frequency-update-algorithm&quot;&gt;2. Fine-Grained Frequency Update Algorithm&lt;/h2&gt;

&lt;p&gt;Based on the previous analysis, we can implement a fine-grained frequency update algorithm. The example we analyzed was a case where the pair to be merged appeared only once in a word. But what if the pair appears multiple times? One approach would be to fall back to the previous algorithm. The possibility of a pair appearing multiple times in a single word is not high. If it appears only once, it can be handled simply; if it appears multiple times, we can fall back to the old, coarse-grained update algorithm—first delete, then insert. I didn’t think of this method before writing the article, which led me to a more complex solution. I created a complicated algorithm that can handle a pair appearing multiple times in one word. Although this algorithm seems perfect, the implementation is much more difficult, and the payoff is not that great. But since I’ve already implemented it, I’ll continue with it.&lt;/p&gt;

&lt;p&gt;For example, consider a hypothetical word &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;abcststefstbbef&quot;&lt;/code&gt;. We can first find all indices where ‘st’ appears. To make it easier to see, we’ll separate ‘st’ from other tokens with a space.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;abc st st ef st bbef
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Based on the previous analysis, the pairs before the first ‘st’ (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;ab&apos;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;bc&apos;&lt;/code&gt;) are unaffected, and the pairs after the last ‘st’ (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;bb&apos;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;be&apos;&lt;/code&gt;) are also unaffected. What about the pairs in the middle? Let’s write them out:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cs st ts st te ef fs st tb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;These pairs first need to be deleted, and then we generate new tokens for the affected section:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c st st e f st b
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we add the new pairs:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(&apos;c&apos;,&apos;st&apos;), (&apos;st&apos;,&apos;st&apos;), (&apos;st&apos;,&apos;e&apos;), (&apos;e&apos;,&apos;f&apos;), (&apos;f&apos;,&apos;st&apos;), (&apos;st&apos;,&apos;b&apos;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Besides decreasing the counts of the affected old pairs and increasing the counts of the new pairs, we also need to modify the inverted index &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_to_words&lt;/code&gt;. My earliest code would simply delete the affected pairs from it. But this has a bug. For example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;ef&apos;&lt;/code&gt; is affected and needs its count reduced. However, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;ef&apos;&lt;/code&gt; also appears at the end of the word, not just in the middle of the three ‘st’ occurrences. If we simply delete &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;ef&apos;&lt;/code&gt; from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_to_words&lt;/code&gt;, the inverted index becomes incorrect. A “true” inverted index not only records that the pair &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;e&apos;, &apos;f&apos;)&lt;/code&gt; appeared in the word &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;abcststefstbbef&apos;&lt;/code&gt; but also how many times it appeared. This would mean that the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_to_words&lt;/code&gt; should be a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dict&lt;/code&gt; mapping words to their pair frequencies, not a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set&lt;/code&gt;. So we only know that the pair &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;e&apos;, &apos;f&apos;)&lt;/code&gt; appeared in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;abcststefstbbef&apos;&lt;/code&gt;, but we don’t know how many times. This is where the problem lies. For example, in the case above, if we simply delete the word &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;abcststefstbbef&apos;&lt;/code&gt; from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_to_words[(&apos;e&apos;,&apos;f&apos;)]&lt;/code&gt;’s value set, the inverted index would be wrong. However, we must delete &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;abcststefstbbef&apos;&lt;/code&gt; from the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_to_words[(&apos;t&apos;,&apos;e&apos;)]&lt;/code&gt;, because the pair &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;t&apos;, &apos;e&apos;)&lt;/code&gt; appears only once in the word &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;abcststefstbbef&apos;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One solution is to change the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_to_words&lt;/code&gt; code to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{pair: {word: word_freq}}&lt;/code&gt; structure. This is a clearer way to do it. However, at the time, I felt that this change would require too many modifications, and I wanted to limit the changes to a single function. So I used another method (which upon reflection is not ideal). This method is: find all the unaffected pairs and put them into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set&lt;/code&gt;. When modifying the inverted index, if the pair to be deleted is also in the set of unaffected pairs, we don’t delete it. Logically, this algorithm is correct, but it makes the code exceptionally complex. And for a case that rarely occurs, it complicates the code and slows down the common cases, which is not a worthwhile trade-off.&lt;/p&gt;

&lt;p&gt;Nonetheless, scanning all the unaffected pairs is relatively fast, and many later improved versions are based on this current algorithm, so modifying it would be a hassle. For the time being, I will not change it in this article.&lt;/p&gt;

&lt;h2 id=&quot;3-code-implementation&quot;&gt;3. Code Implementation&lt;/h2&gt;

&lt;p&gt;The complete code is in &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v5.py&quot;&gt;bpe_v5.py&lt;/a&gt;. Let’s look at the changes. First, the modification to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_updated_affected_word_count&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    @staticmethod
    def _updated_affected_word_count(merge_pair, affected_words, word_encodings, 
                                     word_counts, pair_counts, pair_to_words, 
                                     new_id, pair_strings, vocabulary):
        # we may update/delete words when iterate it.
        affected_words = affected_words.copy()
        diff_pairs = defaultdict(int)

        new_pairs = set() 
        BPE_Trainer.fine_grained_pair_counter_diff(affected_words, word_encodings, word_counts, merge_pair, diff_pairs, 
                             new_id, pair_to_words, new_pairs)
        for pair, count in diff_pairs.items():
            if count == 0: continue
            pair_counts[pair] += count
            if pair_counts[pair] &amp;lt;= 0: # should not less than 0!
                del pair_counts[pair]
                pair_to_words.pop(pair, None)


        for new_pair in new_pairs:
            if new_pair not in pair_strings:
                pair_strings[new_pair] = (vocabulary[new_pair[0]], vocabulary[new_pair[1]])


&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It becomes very simple. It calls a new function, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BPE_Trainer.fine_grained_pair_counter_diff&lt;/code&gt;, to calculate the changes in pair counts after a merge. The main output of this function is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff_pairs&lt;/code&gt;. For example, a hypothetical output might be:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(&apos;a&apos;,&apos;s&apos;): -3
(&apos;t&apos;,&apos;f&apos;): -5
(&apos;st&apos;,&apos;b&apos;):10
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This indicates that due to the merge of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;s&apos;,&apos;t&apos;)&lt;/code&gt;, the count of the old pair &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;a&apos;,&apos;s&apos;)&lt;/code&gt; decreased by 3, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;t&apos;,&apos;f&apos;)&lt;/code&gt; decreased by 5; while the new pair &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;st&apos;,&apos;b&apos;)&lt;/code&gt; increased by 10. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for&lt;/code&gt; loop below then uses this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff&lt;/code&gt; to update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;. Additionally, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BPE_Trainer.fine_grained_pair_counter_diff&lt;/code&gt; function also returns the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new_pairs&lt;/code&gt; that were created after the merge, and we need to update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_strings&lt;/code&gt; with these new pairs as well. Let’s focus on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BPE_Trainer.fine_grained_pair_counter_diff&lt;/code&gt; function.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    @staticmethod
    def fine_grained_pair_counter_diff(affected_words, word_encodings, word_counts, merge_pair, diff_pairs, new_id, pair_to_words, new_pairs):
        for word in affected_words:
            word_tokens = word_encodings[word]
            wc = word_counts[word]

            # find first and last pairs
            idx = 0
            unaffected_pairs = set()
            while idx &amp;lt; len(word_tokens) - 1:
                if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                    first_idx = idx
                    break
                idx += 1
            else:
                print(f&quot;bug {merge_pair}, {word}, {word_tokens}&quot;)
                raise
            # assert first_idx exists

            idx = len(word_tokens) - 2
            while idx &amp;gt; first_idx + 1:
                if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                    last_idx = idx
                    break
                idx -= 1
            else:
                last_idx = first_idx

            start_idx = max(0, first_idx - 1) # inclusive
            end_idx = min(last_idx + 3, len(word_tokens)) # exclusive

            # unaffected [0, start_idx)
            
            for i in range(start_idx):
                pair = word_tokens[i], word_tokens[i + 1]
                unaffected_pairs.add(pair)
            # unaffected [end_idx-1, :-1]
            for i in range(end_idx - 1, len(word_tokens) - 1):
                pair = word_tokens[i], word_tokens[i + 1]
                unaffected_pairs.add(pair)                

            affected_tokens = word_tokens[start_idx: end_idx]
            for i in range(len(affected_tokens) - 1):
                old_pair = (affected_tokens[i], affected_tokens[i + 1])
                diff_pairs[old_pair] -= wc 
                if old_pair not in unaffected_pairs:   
                    pair_to_words[old_pair].discard(word)
                    

            new_tokens = []
            all_new_tokens = []
            for i in range(start_idx):
                all_new_tokens.append(word_tokens[i])
            
            i = 0

            # account for multiple occurrences of the pair
            while i &amp;lt; len(affected_tokens):
                if i &amp;lt; len(affected_tokens) - 1 and (affected_tokens[i], affected_tokens[i + 1]) == merge_pair:
                    new_tokens.append(new_id)
                    all_new_tokens.append(new_id)
                    # jump past pair
                    i += 2
                else:
                    new_tokens.append(affected_tokens[i])
                    all_new_tokens.append(affected_tokens[i])
                    i += 1
            

            for i in range(end_idx, len(word_tokens)):
                all_new_tokens.append(word_tokens[i])
            
            word_encodings[word] = all_new_tokens

            # add new pairs from the updated word
            for i in range(len(new_tokens) - 1):
                new_pair = (new_tokens[i], new_tokens[i + 1])

                diff_pairs[new_pair] += wc
                pair_to_words[new_pair].add(word)

                new_pairs.add(new_pair)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This code is quite complex, especially the index handling. Let’s look at it section by section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding the first and last occurrences of the merged pair&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We first need to find the first and last occurrence of the pair. If it only appears once, these two positions are the same. The range we care about is from the first element of the first occurrence to the last element of the last occurrence.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;            idx = 0
            unaffected_pairs = set()
            while idx &amp;lt; len(word_tokens) - 1:
                if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                    first_idx = idx
                    break
                idx += 1
            else:
                print(f&quot;bug {merge_pair}, {word}, {word_tokens}&quot;)
                raise
            # assert first_idx exists

            idx = len(word_tokens) - 2
            while idx &amp;gt; first_idx + 1:
                if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                    last_idx = idx
                    break
                idx -= 1
            else:
                last_idx = first_idx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The first &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;while&lt;/code&gt; loop finds the first occurrence of the pair and stores its index in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;first_idx&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;else&lt;/code&gt; block should not be executed (unless our inverted index has a bug), so it’s a defensive exception. The second &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;while&lt;/code&gt; loop works backward from the end to find the last occurrence, storing its index in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;last_idx&lt;/code&gt;. If it doesn’t find it, it means the pair only appeared once, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;last_idx&lt;/code&gt; will be the same as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;first_idx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Using the previous example, we find that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;first_idx&lt;/code&gt; is 3 and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;last_idx&lt;/code&gt; is 9. As shown below:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;abcststefstbbef&quot;
 012345678901234
    |     | 
first_idx |
       last_idx  
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, we find all the unaffected pairs:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;            start_idx = max(0, first_idx - 1) # inclusive
            end_idx = min(last_idx + 3, len(word_tokens)) # exclusive

            # unaffected [0, start_idx) 
            for i in range(start_idx):
                pair = word_tokens[i], word_tokens[i + 1]
                unaffected_pairs.add(pair)
                
            # unaffected [end_idx-1, :-1]
            for i in range(end_idx - 1, len(word_tokens) - 1):
                pair = word_tokens[i], word_tokens[i + 1]
                unaffected_pairs.add(pair) 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The index calculations here are particularly tricky, so I won’t go into details. The final result is that the following unaffected pairs are stored in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unaffected_pairs&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ab bc bb be ef
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next is to decrease the frequency of the affected pairs and update the inverted index:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;            affected_tokens = word_tokens[start_idx: end_idx]
            for i in range(len(affected_tokens) - 1):
                old_pair = (affected_tokens[i], affected_tokens[i + 1])
                diff_pairs[old_pair] -= wc 
                if old_pair not in unaffected_pairs:   
                    pair_to_words[old_pair].discard(word)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For our example, assuming &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wc&lt;/code&gt; is 10, the code will execute:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;diff_pairs[(&apos;c&apos;,&apos;s&apos;)] = -10
pair_to_words[(&apos;c&apos;,&apos;s&apos;)].discard(word)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;e&apos;,&apos;f&apos;)&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_to_words[(&apos;e&apos;,&apos;f&apos;)].discard(word)&lt;/code&gt; will not be executed because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;e&apos;,&apos;f&apos;)&lt;/code&gt; is in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unaffected_pairs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, we calculate the new tokens:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;            new_tokens = []
            all_new_tokens = []
            for i in range(start_idx):
                all_new_tokens.append(word_tokens[i])
            
            i = 0

            # account for multiple occurrences of the pair
            while i &amp;lt; len(affected_tokens):
                if i &amp;lt; len(affected_tokens) - 1 and (affected_tokens[i], affected_tokens[i + 1]) == merge_pair:
                    new_tokens.append(new_id)
                    all_new_tokens.append(new_id)
                    # jump past pair
                    i += 2
                else:
                    new_tokens.append(affected_tokens[i])
                    all_new_tokens.append(affected_tokens[i])
                    i += 1
            

            for i in range(end_idx, len(word_tokens)):
                all_new_tokens.append(word_tokens[i])
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note: Previously we only had &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new_tokens&lt;/code&gt;, but now we also have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all_new_tokens&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new_tokens&lt;/code&gt; is for the affected part, while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all_new_tokens&lt;/code&gt; is for the entire word. So for our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;abcststefstbbef&quot;&lt;/code&gt; example, the results are:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;new_tokens = [&apos;c&apos;,&apos;st&apos;,&apos;st&apos;,&apos;e&apos;,&apos;f&apos;,&apos;st&apos;,&apos;b&apos;]
all_new_tokens = [&apos;a&apos;,&apos;b&apos;] + new_tokens + [&apos;b&apos;,&apos;e&apos;,&apos;f&apos;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Finally, we calculate the frequencies of the new pairs:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;            word_encodings[word] = all_new_tokens

            # add new pairs from the updated word
            for i in range(len(new_tokens) - 1):
                new_pair = (new_tokens[i], new_tokens[i + 1])

                diff_pairs[new_pair] += wc
                pair_to_words[new_pair].add(word)

                new_pairs.add(new_pair)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The first line updates &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;word_encodings&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all_new_tokens&lt;/code&gt;. For the example above:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;word_encodings[&apos;abcststefstbbef&apos;] = [&apos;a&apos;,&apos;b&apos;, &apos;c&apos;,&apos;st&apos;,&apos;st&apos;,&apos;e&apos;,&apos;f&apos;,&apos;st&apos;,&apos;b&apos;, &apos;b&apos;,&apos;e&apos;,&apos;f&apos;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then it finds all the new pairs in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new_tokens&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(&apos;c&apos;,&apos;st&apos;)
(&apos;st&apos;,&apos;st&apos;)
(&apos;st&apos;,&apos;e&apos;)
(&apos;e&apos;,&apos;f&apos;)
(&apos;f&apos;,&apos;st&apos;)
(&apos;st&apos;,&apos;b&apos;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here, “new” pairs don’t necessarily mean they didn’t exist before. For example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;e&apos;,&apos;f&apos;)&lt;/code&gt; did exist, but it was deleted and added back, which could result in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff_pairs[(&apos;e&apos;,&apos;f&apos;)]==0&lt;/code&gt;. This is why the code checks if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff_pairs&lt;/code&gt; is zero; if it is, it means there was no change. However, the code above will first delete the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(&apos;e&apos;,&apos;f&apos;)&lt;/code&gt; pair with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff_pairs[old_pair] -= wc&lt;/code&gt; and then add it back later. But this only happens if a pair appears more than twice in a single word. If a pair appears only once, like in the word &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&apos;abcstbbef&apos;&lt;/code&gt;, the deleted pairs are only &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[(&apos;c&apos;,&apos;s&apos;), (&apos;s&apos;,&apos;t&apos;), (&apos;t&apos;,&apos;b&apos;)]&lt;/code&gt;, and it’s impossible for them to be added back.&lt;/p&gt;

&lt;h2 id=&quot;4-testing&quot;&gt;4. Testing&lt;/h2&gt;

&lt;p&gt;The test code is in &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v5_time.py&quot;&gt;bpe_v5_time.py&lt;/a&gt;.&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Version         &lt;/td&gt;
      &lt;td&gt;Data     &lt;/td&gt;
      &lt;td&gt;Total Time (s)&lt;/td&gt;
      &lt;td&gt;Word Freq Time (s)&lt;/td&gt;
      &lt;td&gt;Merge Time (s)&lt;/td&gt;
      &lt;td&gt;Other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v1_time&lt;/td&gt;
      &lt;td&gt;tinystory&lt;/td&gt;
      &lt;td&gt;2187/2264/2247&lt;/td&gt;
      &lt;td&gt;622/642/628&lt;/td&gt;
      &lt;td&gt;count_pair:944/995/984 max:167/173/174/174 update:453/453/460&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v2_time&lt;/td&gt;
      &lt;td&gt;tinystory&lt;/td&gt;
      &lt;td&gt;757/738/746&lt;/td&gt;
      &lt;td&gt;639/621/627&lt;/td&gt;
      &lt;td&gt;118/117/118&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v2_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;35358/34265/35687&lt;/td&gt;
      &lt;td&gt;2870/2949/2930&lt;/td&gt;
      &lt;td&gt;32437/31264/32708&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v3_time&lt;/td&gt;
      &lt;td&gt;tinystory&lt;/td&gt;
      &lt;td&gt;250/90/90&lt;/td&gt;
      &lt;td&gt;80/90/90&lt;/td&gt;
      &lt;td&gt;170/0/0&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1 skip&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v3_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;391/401/32352&lt;/td&gt;
      &lt;td&gt;390/400/410&lt;/td&gt;
      &lt;td&gt;total:31883 max:31187 update:695&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1 skip&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v5_time&lt;/td&gt;
      &lt;td&gt;tinystory&lt;/td&gt;
      &lt;td&gt;221/236/222&lt;/td&gt;
      &lt;td&gt;90/90/90&lt;/td&gt;
      &lt;td&gt;total:130/146/132 max:127/143/129 update:3/3/3&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v5_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;34333/34853/35804&lt;/td&gt;
      &lt;td&gt;401/390&lt;/td&gt;
      &lt;td&gt;total:33879/34401/35347 max:33353/33820/34816 update:525/579/530&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Compared to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_v3_time&lt;/code&gt;, using fine-grained updates on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;openweb&lt;/code&gt; reduces the time from 695 seconds to a little over 500 seconds.&lt;/p&gt;

&lt;h2 id=&quot;5-porting-to-c&quot;&gt;5. Porting to C++&lt;/h2&gt;

&lt;p&gt;Similarly, I ported this fine-grained update algorithm to the C++ versions, including &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained.cpp&quot;&gt;bpe_train_updater_fine_grained&lt;/a&gt;, &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_absl.cpp&quot;&gt;bpe_train_updater_fine_grained_absl&lt;/a&gt;, and &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I won’t go into detail about the code, but interested readers can compare the C++ versions with the Python version.&lt;/p&gt;

&lt;p&gt;The experimental results for these versions are:&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program             &lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;7171/7856/9248&lt;/td&gt;
      &lt;td&gt;392/480/478&lt;/td&gt;
      &lt;td&gt;6779/7376/8770&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;907/908/955&lt;/td&gt;
      &lt;td&gt;514/503/554&lt;/td&gt;
      &lt;td&gt;391/403/400&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=32 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;1268/1196/1215&lt;/td&gt;
      &lt;td&gt;548/473/481&lt;/td&gt;
      &lt;td&gt;719/723/734&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=16 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;2201/2392/2281&lt;/td&gt;
      &lt;td&gt;1931/2120/2010&lt;/td&gt;
      &lt;td&gt;269/272/270&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash2&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1170/1074/1071&lt;/td&gt;
      &lt;td&gt;545/456/449&lt;/td&gt;
      &lt;td&gt;625/617/621&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_absl&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1072/1012/1022&lt;/td&gt;
      &lt;td&gt;423/378/384&lt;/td&gt;
      &lt;td&gt;648/633/637&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_emhash8&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;479/485/485&lt;/td&gt;
      &lt;td&gt;398/401/401&lt;/td&gt;
      &lt;td&gt;80/83/83&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_emhash8&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;469/474/479&lt;/td&gt;
      &lt;td&gt;389/395/399&lt;/td&gt;
      &lt;td&gt;79/78/79&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_emhash8_hash&lt;/td&gt;
      &lt;td&gt;my hash&lt;/td&gt;
      &lt;td&gt;2316/1951/1983&lt;/td&gt;
      &lt;td&gt;2250/1888/1918&lt;/td&gt;
      &lt;td&gt;66/63/64&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;8773/8873/7641&lt;/td&gt;
      &lt;td&gt;220/219/233&lt;/td&gt;
      &lt;td&gt;8552/8653/7408&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_absl&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;845/845/856&lt;/td&gt;
      &lt;td&gt;204/201/203&lt;/td&gt;
      &lt;td&gt;641/643/653&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;After implementing the fine-grained update, the total time for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8&lt;/code&gt; is only &lt;strong&gt;260 seconds&lt;/strong&gt;, with an update time of just 200 seconds. In contrast, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_emhash8&lt;/code&gt; had a total time of 480 seconds, an update time of nearly 400 seconds, and a max time of 79 seconds. This shows that the fine-grained update reduces unnecessary deletions and insertions, cutting the update time by half. These unnecessary operations also tend to make the data structure messy, which is why the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max&lt;/code&gt; time was slightly reduced as well.&lt;/p&gt;

&lt;h2 id=&quot;6-using-emhash8hashsetemhash9hashset-to-replace-stdunordered_set&quot;&gt;6. Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashSet&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash9::HashSet&lt;/code&gt; to Replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_set&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;Besides &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;, another frequently updated data structure is the inverted index &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_wordids&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::unordered_set&amp;lt;int&amp;gt;, pair_hash&amp;gt; pair_wordids;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here, in addition to the outer &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt;, the value itself is an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_set&amp;lt;int&amp;gt;&lt;/code&gt;. We can replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_set&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashSet&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash9::HashSet&lt;/code&gt;. The complete code is available in &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8_set.cpp&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8_set.cpp&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8_set9.cpp&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_fine_grained_emhash8_set9.cpp&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash_set4.hpp&lt;/code&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash9::HashSet&lt;/code&gt;) and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash_table8.hpp&lt;/code&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashMap&lt;/code&gt;) together can cause some compilation warnings. However, according to &lt;a href=&quot;https://github.com/ktprime/emhash/issues/67#issuecomment-3262163325&quot;&gt;this issue&lt;/a&gt;, we can ignore them. The experimental results are as follows:&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program             &lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;7171/7856/9248&lt;/td&gt;
      &lt;td&gt;392/480/478&lt;/td&gt;
      &lt;td&gt;6779/7376/8770&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;907/908/955&lt;/td&gt;
      &lt;td&gt;514/503/554&lt;/td&gt;
      &lt;td&gt;391/403/400&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=32 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;1268/1196/1215&lt;/td&gt;
      &lt;td&gt;548/473/481&lt;/td&gt;
      &lt;td&gt;719/723/734&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=16 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;2201/2392/2281&lt;/td&gt;
      &lt;td&gt;1931/2120/2010&lt;/td&gt;
      &lt;td&gt;269/272/270&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash2&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1170/1074/1071&lt;/td&gt;
      &lt;td&gt;545/456/449&lt;/td&gt;
      &lt;td&gt;625/617/621&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_absl&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1072/1012/1022&lt;/td&gt;
      &lt;td&gt;423/378/384&lt;/td&gt;
      &lt;td&gt;648/633/637&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_emhash8&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;479/485/485&lt;/td&gt;
      &lt;td&gt;398/401/401&lt;/td&gt;
      &lt;td&gt;80/83/83&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_emhash8&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;469/474/479&lt;/td&gt;
      &lt;td&gt;389/395/399&lt;/td&gt;
      &lt;td&gt;79/78/79&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_emhash8_hash&lt;/td&gt;
      &lt;td&gt;my hash&lt;/td&gt;
      &lt;td&gt;2316/1951/1983&lt;/td&gt;
      &lt;td&gt;2250/1888/1918&lt;/td&gt;
      &lt;td&gt;66/63/64&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;8773/8873/7641&lt;/td&gt;
      &lt;td&gt;220/219/233&lt;/td&gt;
      &lt;td&gt;8552/8653/7408&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_absl&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;845/845/856&lt;/td&gt;
      &lt;td&gt;204/201/203&lt;/td&gt;
      &lt;td&gt;641/643/653&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8_set&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;192/192/194&lt;/td&gt;
      &lt;td&gt;117/117/117&lt;/td&gt;
      &lt;td&gt;75/75/77&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8_set9&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;168/170/171&lt;/td&gt;
      &lt;td&gt;107/108/109&lt;/td&gt;
      &lt;td&gt;61/62/61&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;As you can see, by replacing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_wordids&lt;/code&gt; with the faster &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashMap&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashSet&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash9::HashSet&lt;/code&gt;, the time was further reduced from over 260 seconds to around 170 seconds.&lt;/p&gt;

&lt;h2 id=&quot;full-series&quot;&gt;Full Series&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/05/bpe-trainer-0_en/&quot;&gt;Part 0: Introduction&lt;/a&gt; Introduces the basic BPE training algorithm and related tasks, as well as the development environment.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/07/bpe-trainer-1_en/&quot;&gt;Part 1: The Simplest Implementation&lt;/a&gt; The simplest implementation of BPE training.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/08/bpe-trainer-2_en/&quot;&gt;Part 2: Optimized Algorithm&lt;/a&gt; Implements incremental updates for pair_counts.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/09/bpe-trainer-3_en/&quot;&gt;Part 3: Parallel Tokenization and Frequency Counting&lt;/a&gt; Uses multiprocessing to implement a multi-process parallel algorithm.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/10/bpe-trainer-4_en/&quot;&gt;Part 4: A Failed Parallel Optimization&lt;/a&gt; An attempt to parallelize the max pair calculation using multiple processes.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/12/bpe-trainer-5_en/&quot;&gt;Part 5: Implementing the Merge Algorithm in C++&lt;/a&gt; Implements a C++ merge algorithm equivalent to the Python version, and compares two ways of iterating through std::unordered_map.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/15/bpe-trainer-6_en/&quot;&gt;Part 6: Parallelizing the Max Pair Search with OpenMP&lt;/a&gt; Uses OpenMP to find the max pair in pair_counts in parallel.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/18/bpe-trainer-7_en/&quot;&gt;Part 7: Using Flat Hashmap to Replace std::unordered_map&lt;/a&gt; Uses flat hashmap to replace std::unordered_map.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/19/bpe-trainer-8_en/&quot;&gt;Part 8: Implementing Fine-Grained Updates&lt;/a&gt; Implements a fine-grained update algorithm for pair_counts using an inverted index.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/21/bpe-trainer-9_en/&quot;&gt;Part 9: Using a Heap to Find the Max Pair&lt;/a&gt; Uses a heap to find the max pair and improve performance.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/24/bpe-trainer-10_en/&quot;&gt;Part 10: Using Cython and PyPy for Acceleration&lt;/a&gt; Uses Cython and PyPy to accelerate Python code.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/25/bpe-trainer-11_en/&quot;&gt;Part 11: Wrapping C++ Code with Cython&lt;/a&gt; Wraps C++ code using Cython.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Fri, 19 Sep 2025 00:00:00 +0000</pubDate>
        <link>http://fancyerii.github.io/2025/09/19/bpe-trainer-8_en/</link>
        <guid isPermaLink="true">http://fancyerii.github.io/2025/09/19/bpe-trainer-8_en/</guid>
        
        <category>cs336</category>
        
        <category>bpe tokenizer</category>
        
        
      </item>
    
      <item>
        <title>动手实现和优化BPE Tokenizer的训练——第8部分：实现细粒度更新</title>
        <description>&lt;p&gt;本系列文章完成Stanford CS336作业1的一个子任务——实现BPE Tokenizer的高效训练算法。通过一系列优化，我们的算法在OpenWebText上的训练时间从最初的10多个小时优化到小于10分钟。本系列文章解释这一系列优化过程，包括：算法的优化，数据结构的优化，并行(openmp)优化，cython优化，用c++实现关键代码和c++库的cython集成等内容。本文是第九篇，优化pair_counts等数据结构的更新过程。&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#1-问题分析&quot; id=&quot;markdown-toc-1-问题分析&quot;&gt;1. 问题分析&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-细粒度的频率更新算法&quot; id=&quot;markdown-toc-2-细粒度的频率更新算法&quot;&gt;2. 细粒度的频率更新算法&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-代码实现&quot; id=&quot;markdown-toc-3-代码实现&quot;&gt;3. 代码实现&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-测试&quot; id=&quot;markdown-toc-4-测试&quot;&gt;4. 测试&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-移植到c&quot; id=&quot;markdown-toc-5-移植到c&quot;&gt;5. 移植到c++&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-使用emhash8hashsetemhash9hashset替代stdunordered_set&quot; id=&quot;markdown-toc-6-使用emhash8hashsetemhash9hashset替代stdunordered_set&quot;&gt;6. 使用emhash8::HashSet/emhash9::HashSet替代std::unordered_set&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#本系列全部文章&quot; id=&quot;markdown-toc-本系列全部文章&quot;&gt;本系列全部文章&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;1-问题分析&quot;&gt;1. 问题分析&lt;/h2&gt;

&lt;p&gt;我们之前遗留一个优化点，那就是在更新pair_counts时很多pair的计数本来是不需要变的，但是为了实现简单，我们先删除再加入。更具体的，我们来看一下之前的例子。&lt;/p&gt;

&lt;p&gt;假设词频统计的结果为：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{low: 5, lower: 2, widest: 3, newest: 6, es: 2, st: 2}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后统计pair的频率：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{lo: 7, ow: 7, we: 8, er: 2, wi: 3, id: 3, de: 3, es: 11, st: 11, ne: 6, ew: 6}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后根据合并规则，我们选择合并(‘s’, ‘t’)。由于它们发生了合并，我们需要更新pair的频率统计。先看看目前我们的更新算法(代码在&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v2.py#L55&quot;&gt;BPE_Trainer._updated_affected_word_count&lt;/a&gt;，其实更准确的名字应该叫_updated_pair_count_of_affected_word)。&lt;/p&gt;

&lt;p&gt;为了获得新的pair的频率，我们根据pair_to_words找到包含(‘s’, ‘t’)这个pair的word有：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&apos;widest&apos;
&apos;newest&apos;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们首先把这两个词里的pair都先减掉，也就是减去：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{wi: 3, id: 3, de: 3, es: 3, st: 3}
{ne: 6, ew: 6, we: 6, es: 6, st: 6}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样得到：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{lo: 7, ow: 7, we: 2, er: 2, wi: 0, id: 0, de: 0, es: 2, st: 2, ne: 0, ew: 0}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接着这两个词的tokens变成了：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;word_encodings[&apos;widest&apos;] = [&apos;w&apos;,&apos;i&apos;,&apos;d&apos;,&apos;e&apos;,&apos;st&apos;]
word_encodings[&apos;newest&apos;] = [&apos;n&apos;,&apos;e&apos;,&apos;s&apos;,&apos;e&apos;,&apos;st&apos;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;因此我们根据新的word_encodings计算新的pair的频次：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{wi: 3, id: 3, de: 3, est: 3}
{ne: 6, ew: 6, we: 6, est: 6}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;把这些频次加回去得到：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{lo: 7, ow: 7, we: 8, er: 2, wi: 3, id: 3, de: 3, es: 2, st: 2, ne: 6, ew: 6, est: 9}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;可以发现’wi’,’id’和’de’这些pair的频次被减掉又加回去，这就带来没有必要的更新，更糟糕的是，比如’wi’删除后频率为0，那么就需要从pair_counts和pair_to_words里删除。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                    if pair_counts[old_pair] &amp;lt;= 0:
                        # we accounted for all occurrences of this pair
                        del pair_counts[old_pair]
                        pair_to_words.pop(old_pair)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;根据&lt;a href=&quot;https://www.fluentpython.com/extra/internals-of-sets-and-dicts/&quot;&gt;Internals of sets and dicts&lt;/a&gt;，CPython的dict使用的是开放地址的冲突解决方法。从dict里删除一个key只是做一个记号(除非rehash)，然后再增加同样的key会复用原来的地址。所以对dict本身的影响不大，但是key和value还是需要重新构造对象(dict里保存的是key和value的指针)。如果是std::unordered_map这样的分离链接（Separate Chaining）方法，那么删除再插入就会把链表原来的顺序打乱(链表是插入在表头)，比如某个桶原来的元素是a-&amp;gt;b-&amp;gt;c，现在把b删除再插入，新的链表就可能变成b-&amp;gt;a-&amp;gt;c。&lt;/p&gt;

&lt;p&gt;但是如果我们仔细分析，我们就能知道哪些删除是不必要的。比如我们的例子，我们合并的是(‘s’,’t’)，而在’widest’，受到合并影响的除了(‘s’,’t’)之外，只有这个pair之前和之后的token，因此’widest’里的pair受到影响的只有&lt;a href=&quot;&apos;t&apos;后面没有pair了&quot;&gt;‘es’,’st’&lt;/a&gt;，而[‘wi’,’id’,’de’]不受影响，因此我们只需要删除’es’,’st’，然后增加’est’即可。&lt;/p&gt;

&lt;h2 id=&quot;2-细粒度的频率更新算法&quot;&gt;2. 细粒度的频率更新算法&lt;/h2&gt;

&lt;p&gt;根据前面的分析，我们可以实现细粒度的频率更新算法。前面分析的例子是一个要合并的pair在word里只出现一次的情况。但是如果要合并的pair出现多次呢？一种办法是fallback到之前的算法。因为一个word重复出现pair的可能性不大。如果只出现一次，那么可以比较简单的处理；如果出现多次，则fallback到之前的粗粒度的更新算法——先删除再插入。不过我在写文章之前没有想到这种方法，而是把问题搞得比较复杂。弄了一个复杂的算法能够处理pair在一个word里重复出现的算法。虽然这个算法看起来比较完美，但是实现的难度大了很多，而且收益也没有那么大。不过既然已经实现了，那么就继续吧。&lt;/p&gt;

&lt;p&gt;比如一个假想的词”abcststefstbbef”，我们首先可以找到所有出现st的下标，为了看起来简单，我们用空格把st与其它token分开。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;abc st st ef st bbef
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;根据前面的分析，第一个st之前的pair(‘ab’,’bc’)不受影响，最后一个st之后的pair(‘bb’)也不受影响。那么中间的pair呢？我们先写出来：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cs st ts st te ef fs st tb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;这些pair首先得删除，然后我们把受到影响的部分生成新的token：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;c st st e f st b
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们再加入新的pair：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(&apos;c&apos;,&apos;st&apos;), (&apos;st&apos;,&apos;st&apos;), (&apos;st&apos;,&apos;e&apos;), (&apos;e&apos;,&apos;f&apos;), (&apos;f&apos;,&apos;st&apos;), (&apos;st&apos;,&apos;b&apos;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;除了减少受影响的老pair的频次计数，增加新pair的计数，我们还需要修改倒排索引pair_to_words。我最早写的代码是把中间受影响的pair从中删除。但是这是有bug的。比如ef会受到影响，因此需要减少计数。但是ef除了在3个st的中间出现，它还出现在最后。如果我们直接从pair_to_words里删除ef，那么倒排索引就和实际不一致了。”真正”的倒排索引不仅仅记录pair(‘e’,’f’)在词’abcststefstbbef’里出现了，而且还需要记录出现的次数。也就是说pair_to_words的key是pair，value也是dict，value这个dict是word/pair_freq。而我们现在的pair_to_words的value是一个set。因此我们只知道pair(‘e’,’f’)在词’abcststefstbbef’里出现了，但是不知道出现几次。这就出现问题了。比如上面的例子我们之间从pair_to_words[(‘e’,’f’)]里删除词’abcststefstbbef’，那么倒排索引就不对了，但是我们又必须从pair_to_words[(‘t’,’e’)]的value里删除’abcststefstbbef’，因为在单词’abcststefstbbef’里pair(‘t’,’e’)只出现一次。&lt;/p&gt;

&lt;p&gt;一种解决办法就是修改pair_to_words相关代码，把它变成{pair: {word: word_freq}}这样的结构。这是比较清晰的改动方法。不过我当时觉得这么改需要修改的地方太多，我想把修改局限在一个函数里。因此我使用了另外一个办法(现在仔细思考其实并不太好)。这个办法是：找到不受影响的所有pair，把它放到一个set里。在修改倒排索引时如果发现要删除的pair在不受影响的pair也出现，那么不删除。从逻辑的角度这个算法也是正确的，但是它使得代码变得复杂。&lt;/p&gt;

&lt;p&gt;不过总体来说扫描一遍不受影响的pair速度也比较快，而且后面的很多改进版本都基于当前算法，要改动比较麻烦。所以在写这篇文章的时候我就暂时不修改了。&lt;/p&gt;

&lt;h2 id=&quot;3-代码实现&quot;&gt;3. 代码实现&lt;/h2&gt;

&lt;p&gt;完整的代码在&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v5.py&quot;&gt;bpe_v5.py&lt;/a&gt;。我们看一下改动的地方。首先是对_updated_affected_word_count的修改：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    @staticmethod
    def _updated_affected_word_count(merge_pair, affected_words, word_encodings, 
                                     word_counts, pair_counts, pair_to_words, 
                                     new_id, pair_strings, vocabulary):
        # we may update/delete words when iterate it.
        affected_words = affected_words.copy()
        diff_pairs = defaultdict(int)

        new_pairs = set() 
        BPE_Trainer.fine_grained_pair_counter_diff(affected_words, word_encodings, word_counts, merge_pair, diff_pairs, 
                             new_id, pair_to_words, new_pairs)
        for pair, count in diff_pairs.items():
            if count == 0: continue
            pair_counts[pair] += count
            if pair_counts[pair] &amp;lt;= 0: # should not less than 0!
                del pair_counts[pair]
                pair_to_words.pop(pair, None)


        for new_pair in new_pairs:
            if new_pair not in pair_strings:
                pair_strings[new_pair] = (vocabulary[new_pair[0]], vocabulary[new_pair[1]])


&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;它调用一个新的函数BPE_Trainer._updated_affected_word_count变得非常简单，它调用一个新的函数BPE_Trainer.fine_grained_pair_counter_diff来计算合并pair后pair计数的变化。这个函数的主要输出是diff_pairs，比如假想的一个输出：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(&apos;a&apos;,&apos;s&apos;): -3
(&apos;t&apos;,&apos;f&apos;): -5
(&apos;st&apos;,&apos;b&apos;):10
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;则它说明由于合并(‘s’,’t’)，导致老的pair(‘a’,’s’)的计数减少了3，(‘t’,’f’)减少了5；而新的pair(‘st’,’b’)增加了10。所以下面的for循环就用这个diff去更新pair_counts。另外BPE_Trainer.fine_grained_pair_counter_diff函数也会返回合并后新增的new_pairs，这些新出现的pair，我们也需要更新pair_strings。下面我们重点来看BPE_Trainer.fine_grained_pair_counter_diff函数。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    @staticmethod
    def fine_grained_pair_counter_diff(affected_words, word_encodings, word_counts, merge_pair, diff_pairs, new_id, pair_to_words, new_pairs):
        for word in affected_words:
            word_tokens = word_encodings[word]
            wc = word_counts[word]

            # find first and last pairs
            idx = 0
            unaffected_pairs = set()
            while idx &amp;lt; len(word_tokens) - 1:
                if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                    first_idx = idx
                    break
                idx += 1
            else:
                print(f&quot;bug {merge_pair}, {word}, {word_tokens}&quot;)
                raise
            # assert first_idx exists

            idx = len(word_tokens) - 2
            while idx &amp;gt; first_idx + 1:
                if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                    last_idx = idx
                    break
                idx -= 1
            else:
                last_idx = first_idx

            start_idx = max(0, first_idx - 1) # inclusive
            end_idx = min(last_idx + 3, len(word_tokens)) # exclusive

            # unaffected [0, start_idx)
            
            for i in range(start_idx):
                pair = word_tokens[i], word_tokens[i + 1]
                unaffected_pairs.add(pair)
            # unaffected [end_idx-1, :-1]
            for i in range(end_idx - 1, len(word_tokens) - 1):
                pair = word_tokens[i], word_tokens[i + 1]
                unaffected_pairs.add(pair)                

            affected_tokens = word_tokens[start_idx: end_idx]
            for i in range(len(affected_tokens) - 1):
                old_pair = (affected_tokens[i], affected_tokens[i + 1])
                diff_pairs[old_pair] -= wc 
                if old_pair not in unaffected_pairs:   
                    pair_to_words[old_pair].discard(word)
                    

            new_tokens = []
            all_new_tokens = []
            for i in range(start_idx):
                all_new_tokens.append(word_tokens[i])
            
            i = 0

            # account for multiple occurrences of the pair
            while i &amp;lt; len(affected_tokens):
                if i &amp;lt; len(affected_tokens) - 1 and (affected_tokens[i], affected_tokens[i + 1]) == merge_pair:
                    new_tokens.append(new_id)
                    all_new_tokens.append(new_id)
                    # jump past pair
                    i += 2
                else:
                    new_tokens.append(affected_tokens[i])
                    all_new_tokens.append(affected_tokens[i])
                    i += 1
            

            for i in range(end_idx, len(word_tokens)):
                all_new_tokens.append(word_tokens[i])
            
            word_encodings[word] = all_new_tokens

            # add new pairs from the updated word
            for i in range(len(new_tokens) - 1):
                new_pair = (new_tokens[i], new_tokens[i + 1])

                diff_pairs[new_pair] += wc
                pair_to_words[new_pair].add(word)

                new_pairs.add(new_pair)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这段代码比较复杂，尤其是下标的处理。我们分段来仔细看一下。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;找到合并pair的第一次和最后一次出现&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我们首先需要找到pair的第一次和最后一次出现的位置，如果只有一次，那么这两个位置是相同的。pair第一次出现的第一个元素和第二次出现第二个元素就是我们关注的范围。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;            idx = 0
            unaffected_pairs = set()
            while idx &amp;lt; len(word_tokens) - 1:
                if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                    first_idx = idx
                    break
                idx += 1
            else:
                print(f&quot;bug {merge_pair}, {word}, {word_tokens}&quot;)
                raise
            # assert first_idx exists

            idx = len(word_tokens) - 2
            while idx &amp;gt; first_idx + 1:
                if word_tokens[idx] == merge_pair[0] and word_tokens[idx+1] == merge_pair[1]:
                    last_idx = idx
                    break
                idx -= 1
            else:
                last_idx = first_idx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;第一个while循环找到pair出现的第一次位置，记录在first_idx里。else是不应该运行的(除非我们的倒排索引出现问题)，这是防御性的抛出异常。第二个while循环反过来从最后的位置开始找，记录在last_idx里，如果找不到则说明这个pair只出现了一次，则last_idx==first_idx。&lt;/p&gt;

&lt;p&gt;用之前的例子来分析，我们找到的first_idx是3，last_idx是9。如下所示：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;abcststefstbbef&quot;
 012345678901234
    |     | 
first_idx |
       last_idx  
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接下来是找到所有不受影响的pair：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;            start_idx = max(0, first_idx - 1) # inclusive
            end_idx = min(last_idx + 3, len(word_tokens)) # exclusive

            # unaffected [0, start_idx) 
            for i in range(start_idx):
                pair = word_tokens[i], word_tokens[i + 1]
                unaffected_pairs.add(pair)
                
            # unaffected [end_idx-1, :-1]
            for i in range(end_idx - 1, len(word_tokens) - 1):
                pair = word_tokens[i], word_tokens[i + 1]
                unaffected_pairs.add(pair) 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里的下标计算特别要小心，我就不细讲了，总之最终的结果是找到如下不受影响的pair保存的unaffected_pairs里：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ab bc bb be ef
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接下来就是减少受影响的pair的频次和更新倒排索引：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;            affected_tokens = word_tokens[start_idx: end_idx]
            for i in range(len(affected_tokens) - 1):
                old_pair = (affected_tokens[i], affected_tokens[i + 1])
                diff_pairs[old_pair] -= wc 
                if old_pair not in unaffected_pairs:   
                    pair_to_words[old_pair].discard(word)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;比如我们的例子，假设wc是10，则会执行：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;diff_pairs[(&apos;c&apos;,&apos;s&apos;)] = -10
pair_to_words[(&apos;c&apos;,&apos;s&apos;)].discard(word)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;而对于(‘e’,’f’)，pair_to_words[(‘e’,’f’)].discard(word)不会执行，因为(‘e’,’f’)在unaffected_pairs里。&lt;/p&gt;

&lt;p&gt;接下来是计算新的tokens：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;            new_tokens = []
            all_new_tokens = []
            for i in range(start_idx):
                all_new_tokens.append(word_tokens[i])
            
            i = 0

            # account for multiple occurrences of the pair
            while i &amp;lt; len(affected_tokens):
                if i &amp;lt; len(affected_tokens) - 1 and (affected_tokens[i], affected_tokens[i + 1]) == merge_pair:
                    new_tokens.append(new_id)
                    all_new_tokens.append(new_id)
                    # jump past pair
                    i += 2
                else:
                    new_tokens.append(affected_tokens[i])
                    all_new_tokens.append(affected_tokens[i])
                    i += 1
            

            for i in range(end_idx, len(word_tokens)):
                all_new_tokens.append(word_tokens[i])
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;注意：之前我们只有一个new_tokens，但是现在我们还有一个all_new_tokens。new_tokens是计算受影响的那部分，而all_new_tokens是整个word。因此对于我们之前”abcststefstbbef”的例子，它们的结果是：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;new_tokens = [&apos;c&apos;,&apos;st&apos;,&apos;st&apos;,&apos;e&apos;,&apos;f&apos;,&apos;st&apos;,&apos;b&apos;]
all_new_tokens = [&apos;a&apos;,&apos;b&apos;] + new_tokens + [&apos;b&apos;,&apos;e&apos;,&apos;f&apos;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;最后是计算新的pair的频次：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;            word_encodings[word] = all_new_tokens

            # add new pairs from the updated word
            for i in range(len(new_tokens) - 1):
                new_pair = (new_tokens[i], new_tokens[i + 1])

                diff_pairs[new_pair] += wc
                pair_to_words[new_pair].add(word)

                new_pairs.add(new_pair)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;第一行是更新word_encodings为all_new_tokens。比如前面的例子：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;word_encodings[&apos;abcststefstbbef&apos;] = [&apos;a&apos;,&apos;b&apos;, &apos;c&apos;,&apos;st&apos;,&apos;st&apos;,&apos;e&apos;,&apos;f&apos;,&apos;st&apos;,&apos;b&apos;, &apos;b&apos;,&apos;e&apos;,&apos;f&apos;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接着是找到new_tokens里的所有新pair：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(&apos;c&apos;,&apos;st&apos;)
(&apos;st&apos;,&apos;st&apos;)
(&apos;st&apos;,&apos;e&apos;)
(&apos;e&apos;,&apos;f&apos;)
(&apos;f&apos;,&apos;st&apos;)
(&apos;st&apos;,&apos;b&apos;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里所谓的新并不一定就是之前没有出现的pair，比如(‘e’,’f’)之前其实就有，只不过被删除又加进来，因此可能出现diff_pairs[(‘e’,’f’)]==0的情况，这也是前面代码会判断diff_pairs是否为0，如果为0，则说明它并没有改变。但是上面的代码会在前面diff_pairs[old_pair] -= wc先删除(‘e’,’f’)这个pair，后面又再加进来。不过这种情况只有在一个word里包含pair两次以上才会出现。如果pair只出现一次，比如单词是’abcstbbef’，那么被删除的只有[(‘c’,’s’), (‘s’,’t’), (‘t’,’b’)]，这些如果被删除就不可能再被加进来。&lt;/p&gt;

&lt;h2 id=&quot;4-测试&quot;&gt;4. 测试&lt;/h2&gt;

&lt;p&gt;测试的代码是&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cs336_basics/bpe_v5_time.py&quot;&gt;bpe_v5_time.py&lt;/a&gt;。&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;数据&lt;/td&gt;
      &lt;td&gt;总时间(s)&lt;/td&gt;
      &lt;td&gt;统计词频时间(s)&lt;/td&gt;
      &lt;td&gt;合并时间(s)&lt;/td&gt;
      &lt;td&gt;其它&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v1_time&lt;/td&gt;
      &lt;td&gt;tinystory&lt;/td&gt;
      &lt;td&gt;2187/2264/2247&lt;/td&gt;
      &lt;td&gt;622/642/628&lt;/td&gt;
      &lt;td&gt;count_pair:944/995/984 max:167/173/174/174 update:453/453/460&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v2_time&lt;/td&gt;
      &lt;td&gt;tinystory&lt;/td&gt;
      &lt;td&gt;757/738/746&lt;/td&gt;
      &lt;td&gt;639/621/627&lt;/td&gt;
      &lt;td&gt;118/117/118&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v2_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;35358/34265/35687&lt;/td&gt;
      &lt;td&gt;2870/2949/2930&lt;/td&gt;
      &lt;td&gt;32437/31264/32708&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v3_time&lt;/td&gt;
      &lt;td&gt;tinystory&lt;/td&gt;
      &lt;td&gt;250/90/90&lt;/td&gt;
      &lt;td&gt;80/90/90&lt;/td&gt;
      &lt;td&gt;170/0/0&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1 skip&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v3_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;391/401/32352&lt;/td&gt;
      &lt;td&gt;390/400/410&lt;/td&gt;
      &lt;td&gt;total:31883 max:31187 update:695&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1 skip&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v5_time&lt;/td&gt;
      &lt;td&gt;tinystory&lt;/td&gt;
      &lt;td&gt;221/236/222&lt;/td&gt;
      &lt;td&gt;90/90/90&lt;/td&gt;
      &lt;td&gt;total:130/146/132 max:127/143/129 update:3/3/3&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_v5_time&lt;/td&gt;
      &lt;td&gt;openweb&lt;/td&gt;
      &lt;td&gt;34333/34853/35804&lt;/td&gt;
      &lt;td&gt;401/390/401&lt;/td&gt;
      &lt;td&gt;total:33879/34401/35347 max:33353/33820/34816 update:525/579/530&lt;/td&gt;
      &lt;td&gt;num_counter=8, num_merger=1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;和bpe_v3_time相比，使用细粒度的更新在openweb上的时间从695秒下降到500多秒。&lt;/p&gt;

&lt;h2 id=&quot;5-移植到c&quot;&gt;5. 移植到c++&lt;/h2&gt;

&lt;p&gt;同样的，我把这个细粒度更新的算法移植到了c++的版本，包括&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained.cpp&quot;&gt;bpe_train_updater_fine_grained&lt;/a&gt;、&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_absl.cpp&quot;&gt;bpe_train_updater_fine_grained_absl&lt;/a&gt;和&lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8.cpp&quot;&gt;bpe_train_updater_fine_grained_emhash8&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;代码就不详细介绍，感兴趣的读者可以对照Python的版本阅读c++版本的代码。&lt;/p&gt;

&lt;p&gt;这些版本的实验结果为：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program&lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;7171/7856/9248&lt;/td&gt;
      &lt;td&gt;392/480/478&lt;/td&gt;
      &lt;td&gt;6779/7376/8770&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;907/908/955&lt;/td&gt;
      &lt;td&gt;514/503/554&lt;/td&gt;
      &lt;td&gt;391/403/400&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=32 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;1268/1196/1215&lt;/td&gt;
      &lt;td&gt;548/473/481&lt;/td&gt;
      &lt;td&gt;719/723/734&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=16 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;2201/2392/2281&lt;/td&gt;
      &lt;td&gt;1931/2120/2010&lt;/td&gt;
      &lt;td&gt;269/272/270&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash2&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1170/1074/1071&lt;/td&gt;
      &lt;td&gt;545/456/449&lt;/td&gt;
      &lt;td&gt;625/617/621&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_absl&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1072/1012/1022&lt;/td&gt;
      &lt;td&gt;423/378/384&lt;/td&gt;
      &lt;td&gt;648/633/637&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_emhash8&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;479/485/485&lt;/td&gt;
      &lt;td&gt;398/401/401&lt;/td&gt;
      &lt;td&gt;80/83/83&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_emhash8&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;469/474/479&lt;/td&gt;
      &lt;td&gt;389/395/399&lt;/td&gt;
      &lt;td&gt;79/78/79&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_emhash8_hash&lt;/td&gt;
      &lt;td&gt;my hash&lt;/td&gt;
      &lt;td&gt;2316/1951/1983&lt;/td&gt;
      &lt;td&gt;2250/1888/1918&lt;/td&gt;
      &lt;td&gt;66/63/64&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;8773/8873/7641&lt;/td&gt;
      &lt;td&gt;220/219/233&lt;/td&gt;
      &lt;td&gt;8552/8653/7408&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_absl&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;845/845/856&lt;/td&gt;
      &lt;td&gt;204/201/203&lt;/td&gt;
      &lt;td&gt;641/643/653&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;使用了细粒度更新后，bpe_train_updater_fine_grained_emhash8的总时间只有&lt;strong&gt;260秒&lt;/strong&gt;，其中更新时间只有200秒。对比bpe_train_updater_emhash8，它的总时间是480秒，update时间接近400秒，max时间79秒。这说明细粒度更新减少了不必要的删除和插入，因此update时间变为原来的一半，而且这些不必要的删除和插入也会让数据结构变得混乱，因此max的时间也有少量减少。&lt;/p&gt;

&lt;h2 id=&quot;6-使用emhash8hashsetemhash9hashset替代stdunordered_set&quot;&gt;6. 使用emhash8::HashSet/emhash9::HashSet替代std::unordered_set&lt;/h2&gt;

&lt;p&gt;除了pair_counts，还有一个比较频繁更新的就是倒排索引pair_wordids:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;std::unordered_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, std::unordered_set&amp;lt;int&amp;gt;, pair_hash&amp;gt; pair_wordids;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里除了外面的std::unordered_map，value本身也是一个std::unordered_set&lt;int&gt;。我们可以用emhash8::HashSet/emhash9::HashSet替代std::unordered_set。完整的代码在[bpe_train_updater_fine_grained_emhash8_set.cpp](https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8_set.cpp)和[bpe_train_updater_fine_grained_emhash8_set9.cpp](https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_fine_grained_emhash8_set9.cpp)。&lt;/int&gt;&lt;/p&gt;

&lt;p&gt;同时使用hash_set4.hpp(emhash9::HashSet) and hash_table8.hpp(emhash8::HashMap)在编译时会出现一些警告，不过根据&lt;a href=&quot;https://github.com/ktprime/emhash/issues/67#issuecomment-3262163325&quot;&gt;这个issue&lt;/a&gt;，我们可以忽略它们。实验结果如下：&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program&lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;7171/7856/9248&lt;/td&gt;
      &lt;td&gt;392/480/478&lt;/td&gt;
      &lt;td&gt;6779/7376/8770&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;907/908/955&lt;/td&gt;
      &lt;td&gt;514/503/554&lt;/td&gt;
      &lt;td&gt;391/403/400&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=32 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;1268/1196/1215&lt;/td&gt;
      &lt;td&gt;548/473/481&lt;/td&gt;
      &lt;td&gt;719/723/734&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=16 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;2201/2392/2281&lt;/td&gt;
      &lt;td&gt;1931/2120/2010&lt;/td&gt;
      &lt;td&gt;269/272/270&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash2&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1170/1074/1071&lt;/td&gt;
      &lt;td&gt;545/456/449&lt;/td&gt;
      &lt;td&gt;625/617/621&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_absl&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1072/1012/1022&lt;/td&gt;
      &lt;td&gt;423/378/384&lt;/td&gt;
      &lt;td&gt;648/633/637&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_emhash8&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;479/485/485&lt;/td&gt;
      &lt;td&gt;398/401/401&lt;/td&gt;
      &lt;td&gt;80/83/83&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_emhash8&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;469/474/479&lt;/td&gt;
      &lt;td&gt;389/395/399&lt;/td&gt;
      &lt;td&gt;79/78/79&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_emhash8_hash&lt;/td&gt;
      &lt;td&gt;my hash&lt;/td&gt;
      &lt;td&gt;2316/1951/1983&lt;/td&gt;
      &lt;td&gt;2250/1888/1918&lt;/td&gt;
      &lt;td&gt;66/63/64&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;8773/8873/7641&lt;/td&gt;
      &lt;td&gt;220/219/233&lt;/td&gt;
      &lt;td&gt;8552/8653/7408&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_absl&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;845/845/856&lt;/td&gt;
      &lt;td&gt;204/201/203&lt;/td&gt;
      &lt;td&gt;641/643/653&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;261/259/261&lt;/td&gt;
      &lt;td&gt;200/198/200&lt;/td&gt;
      &lt;td&gt;61/60/60&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8_set&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;192/192/194&lt;/td&gt;
      &lt;td&gt;117/117/117&lt;/td&gt;
      &lt;td&gt;75/75/77&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_fine_grained_emhash8_set9&lt;/td&gt;
      &lt;td&gt;Boost Hash&lt;/td&gt;
      &lt;td&gt;168/170/171&lt;/td&gt;
      &lt;td&gt;107/108/109&lt;/td&gt;
      &lt;td&gt;61/62/61&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;可以看到pair_wordids用更快的emhash8::HashMap/emhash8::HashSet/emhash9::HashSet替换会后，时间进一步从260多秒降到了170秒。&lt;/p&gt;

&lt;h2 id=&quot;本系列全部文章&quot;&gt;本系列全部文章&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/05/bpe-trainer-0/&quot;&gt;第0部分：简介&lt;/a&gt; 介绍bpe训练的基本算法和相关任务，并且介绍开发环境。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/07/bpe-trainer-1/&quot;&gt;第1部分：最简单实现&lt;/a&gt; bpe训练最简单的实现。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/08/bpe-trainer-2/&quot;&gt;第2部分：优化算法&lt;/a&gt; 实现pair_counts的增量更新。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/09/bpe-trainer-3/&quot;&gt;第3部分：并行分词和统计词频&lt;/a&gt; 使用multiprocessing实现多进程并行算法。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/10/bpe-trainer-4/&quot;&gt;第4部分：一次失败的并行优化&lt;/a&gt; 尝试用多进程并行计算max pair。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/12/bpe-trainer-5/&quot;&gt;第5部分：用C++实现Merge算法&lt;/a&gt; 用C++实现和Python等价的merge算法，并且比较std::unordered_map的两种遍历方式。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/15/bpe-trainer-6/&quot;&gt;第6部分：用OpenMP实现并行求最大&lt;/a&gt; 用OpenMP并行求pair_counts里最大pair。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/18/bpe-trainer-7/&quot;&gt;第7部分：使用flat hashmap替代std::unordered_map&lt;/a&gt; 使用flat hashmap来替代std::unordered_map。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/19/bpe-trainer-8/&quot;&gt;第8部分：实现细粒度更新&lt;/a&gt; 使用倒排索引实现pair_counts的细粒度更新算法。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/21/bpe-trainer-9/&quot;&gt;第9部分：使用堆来寻找最大pair&lt;/a&gt; 使用堆来求最大pair，提升性能。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/24/bpe-trainer-10/&quot;&gt;第10部分：使用cython和pypy来加速&lt;/a&gt; 使用cython和pypy来加速python代码。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/25/bpe-trainer-11/&quot;&gt;第11部分：使用cython封装c++代码&lt;/a&gt; 使用cython封装c++代码。&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Fri, 19 Sep 2025 00:00:00 +0000</pubDate>
        <link>http://fancyerii.github.io/2025/09/19/bpe-trainer-8/</link>
        <guid isPermaLink="true">http://fancyerii.github.io/2025/09/19/bpe-trainer-8/</guid>
        
        <category>cs336</category>
        
        <category>bpe tokenizer</category>
        
        
      </item>
    
      <item>
        <title>Implementing and Optimizing a BPE Tokenizer from Scratch—Part 7: Using Flat Hash Map instead of std::unordered_map</title>
        <description>&lt;p&gt;This series of articles implements a subtask of Stanford’s CS336 Assignment 1: building an efficient training algorithm for a BPE Tokenizer. Through a series of optimizations, our algorithm’s training time on OpenWebText was reduced from over 10 hours to less than 10 minutes. This series explains these optimizations, including algorithmic improvements, data structure enhancements, parallelization with OpenMP, Cython optimization, and implementing key code in C++ along with its integration via Cython. This is the eighth article in the series, focusing on using a &lt;strong&gt;flat hash map&lt;/strong&gt; to replace the C++ standard library’s &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt;&lt;/strong&gt; for improved performance.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;strong&gt;Table of Content&lt;/strong&gt;&lt;/p&gt;
&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#1-problem-analysis&quot; id=&quot;markdown-toc-1-problem-analysis&quot;&gt;1. Problem Analysis&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-optimizing-the---operation&quot; id=&quot;markdown-toc-2-optimizing-the---operation&quot;&gt;2. Optimizing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-=&lt;/code&gt; Operation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-flat-hash-map-survey&quot; id=&quot;markdown-toc-3-flat-hash-map-survey&quot;&gt;3. Flat Hash Map Survey&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-abslflat_hash_map&quot; id=&quot;markdown-toc-4-abslflat_hash_map&quot;&gt;4. absl::flat_hash_map&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-abslflat_hash_map-test&quot; id=&quot;markdown-toc-5-abslflat_hash_map-test&quot;&gt;5. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt; Test&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-emhash8hashmap&quot; id=&quot;markdown-toc-6-emhash8hashmap&quot;&gt;6. emhash8::HashMap&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#7-emhash8hashmap-test-results&quot; id=&quot;markdown-toc-7-emhash8hashmap-test-results&quot;&gt;7. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashMap&lt;/code&gt; Test Results&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#8-summary&quot; id=&quot;markdown-toc-8-summary&quot;&gt;8. Summary&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#full-series&quot; id=&quot;markdown-toc-full-series&quot;&gt;Full Series&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;1-problem-analysis&quot;&gt;1. Problem Analysis&lt;/h2&gt;

&lt;p&gt;In the previous article, we used OpenMP parallelization to reduce the time for the second merge step from over 6000 seconds to less than 1000 seconds with 32 threads (bpe_train_updater_omp_v7). Out of that 1000 seconds, over 500 seconds were spent on updating data like pair_counts, and another 400+ seconds were for finding the max pair.&lt;/p&gt;

&lt;p&gt;Before continuing the optimization, let’s review the main data read/write operations in the function &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_step2&lt;/code&gt;. We’ll use the serial version of the code, &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_omp_v3.cpp%23L232&quot;&gt;bpe_train_updater_omp_v3.cpp&lt;/a&gt;, as an example.&lt;/p&gt;

&lt;p&gt;The main code for finding the max pair iterates through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;. It only needs to read &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_strings[pair]&lt;/code&gt; when the count of the current pair is the same as the current maximum.&lt;/p&gt;

&lt;p&gt;The code for updating variables like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; is as follows:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;        const int token_size = word_tokens.size();
        for(int i = 0; i &amp;lt; token_size - 1; ++i){
            std::pair&amp;lt;int, int&amp;gt; old_pair(word_tokens[i], word_tokens[i + 1]);
            pair_counts[old_pair] -= wc;
            if(pair_counts[old_pair] &amp;lt;= 0){
                pair_counts.erase(old_pair);
                pair_wordids.erase(old_pair);
            }else{
                pair_wordids[old_pair].erase(wordid);
            }
        }
        
        
        for(int i = 0; i &amp;lt; new_tokens_size - 1; ++i){
            std::pair&amp;lt;int, int&amp;gt; new_pair(new_tokens[i], new_tokens[i + 1]);
            pair_counts[new_pair] += wc;
            pair_wordids[new_pair].insert(wordid);
            if (pair_strings.find(new_pair) == pair_strings.end()) {
                pair_strings[new_pair] = {vocabulary[new_pair.first], vocabulary[new_pair.second]};
            }
        }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The most critical operations here are the write operations: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts[old_pair] -= wc;&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts[new_pair] += wc;&lt;/code&gt;. Additionally, when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts[old_pair] &amp;lt;= 0&lt;/code&gt;, the code deletes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;old_pair&lt;/code&gt; from both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_wordids&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is also a read operation &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if(pair_counts[old_pair] &amp;lt;= 0)&lt;/code&gt;. This operation can actually be optimized away, which we’ll discuss later. But even without optimization, the time it takes is very short. First, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unordered_map&lt;/code&gt; lookups are fast (updates require memory allocation, insertion into a linked list and bucket, and may cause a rehash). Second, this lookup happens right after an update, so the relevant data should still be in the CPU’s cache.&lt;/p&gt;

&lt;p&gt;First, let’s look at iterating through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;. In the serial version, we iterate through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unordered_map&lt;/code&gt;’s internal singly-linked lists. In the parallel version, we also traverse the linked list contents, but we do so in chunks using the bucket interface. From an implementation perspective, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unordered_map&lt;/code&gt; is a linked list, which allows for O(1) time complexity for element insertion and deletion. However, our main write operation is updating the value corresponding to a key. The memory layout of a linked list is very fragmented, and its traversal is much slower than a contiguous memory layout. Therefore, for our scenario, we can use a hash map with a contiguous layout, a so-called &lt;strong&gt;flat hash map&lt;/strong&gt;. Its main feature is that it stores all data in a single, contiguous block of memory, unlike a standard hash map which disperses data using linked lists or pointers.&lt;/p&gt;

&lt;p&gt;A standard hash map (e.g., C++’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt;) typically uses a hash function to map keys to different “buckets,” where each bucket might be a linked list to store elements with the same hash value. This results in elements being scattered and non-contiguous in memory.&lt;/p&gt;

&lt;p&gt;A flat hash map, on the other hand, is completely different. Its core principles are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Contiguous Memory Layout&lt;/strong&gt;: It uses one large array to store all key-value pairs. Since the data is stored contiguously, this significantly improves &lt;strong&gt;Cache-Friendliness&lt;/strong&gt;. When a processor accesses one element, it usually loads nearby elements into the cache as well, making subsequent accesses much faster.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Open Addressing&lt;/strong&gt;: To handle hash collisions (when two different keys hash to the same location), a flat hash map does not use linked lists. Instead, it employs a technique called &lt;strong&gt;Probing&lt;/strong&gt;. When a position is already occupied, it follows a predefined rule (e.g., linear or quadratic probing) to find the next available empty slot for the data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pros and Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Pros&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Excellent Cache Performance&lt;/strong&gt;: This is its biggest advantage. In modern CPUs, cache access is much faster than main memory access. Because data is contiguous, a flat hash map’s access pattern is ideal for the CPU cache, especially for traversal or bulk lookups.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Low Memory Overhead&lt;/strong&gt;: Since it doesn’t need to store extra pointers for each element, its memory footprint is smaller, especially when storing a large number of small elements.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Ideal for High-Performance Scenarios&lt;/strong&gt;: In fields with extremely low latency requirements, such as game development, network programming, and high-performance computing, the flat hash map is a great choice.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cons&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Complex Deletion&lt;/strong&gt;: Due to the probing mechanism, elements cannot simply be removed. A special “tombstone” marker is needed to ensure that subsequent lookups in the chain are not broken. This can lead to fragmentation in the array and requires periodic cleanup (re-hashing).&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Clustering Issues&lt;/strong&gt;: If the hash function is not ideal or if collisions are frequent, data can cluster together in the array, forming “blocks.” This makes the probing process longer and can degrade performance.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;High Rehash Cost&lt;/strong&gt;: When the underlying array needs to be resized, a larger new array must be allocated and all elements copied over, a process with a relatively high cost.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, for our specific scenario, a flat hash map is very suitable: our main operation is traversal, and deletions are infrequent.&lt;/p&gt;

&lt;h2 id=&quot;2-optimizing-the---operation&quot;&gt;2. Optimizing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-=&lt;/code&gt; Operation&lt;/h2&gt;

&lt;p&gt;Before replacing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt; with a flat hash map, let’s do a small optimization to save one query to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In C++, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unordered_map&lt;/code&gt;’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-=&lt;/code&gt; is an &lt;strong&gt;expression&lt;/strong&gt; that returns the value after the operation; whereas in Python, a dict’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-=&lt;/code&gt; is a &lt;strong&gt;statement&lt;/strong&gt; with no return value. For example, the following code compiles in C++:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;int x = 1;
int y = (x += 1);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But similar code in Python:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;x = 1
y = (x += 1)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;will cause a syntax error:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    y = (x += 1)
           ^
SyntaxError: invalid syntax
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So in Python, we can only query &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts[old_pair]&lt;/code&gt; again after the update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pair_counts[old_pair] -= wc;&lt;/code&gt;. But in C++, we can directly get the value after the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-=&lt;/code&gt; operation.&lt;/p&gt;

&lt;p&gt;We can apply this optimization to all versions (e.g., bpe_train_updater_omp_v3/bpe_train_updater_omp_v5/bpe_train_updater_omp_v7, etc.), but since we only discovered it after the last version, we’ll first compare it with the original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater.cpp&lt;/code&gt;. If it proves effective, we’ll apply it to the final version. The complete code is in &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_opt.cpp#L165&quot;&gt;bpe_train_updater_opt.cpp&lt;/a&gt;, with only one change:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;            int&amp;amp; c = (pair_counts[old_pair] -= wc);
            if(c &amp;lt;= 0){
                pair_counts.erase(old_pair);
                pair_wordids.erase(old_pair);
            }else{
                pair_wordids[old_pair].erase(wordid);
            }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s the test comparison:&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program             &lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;7171/7856/9248&lt;/td&gt;
      &lt;td&gt;392/480/478&lt;/td&gt;
      &lt;td&gt;6779/7376/8770&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;8153/7634/7362&lt;/td&gt;
      &lt;td&gt;411/396/389&lt;/td&gt;
      &lt;td&gt;7741/7238/6973&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Comparing the update times, we see an average decrease from 450s to 398.67s, an improvement of 11.41%. While this may not be a huge gain for the overall runtime, every little bit helps.&lt;/p&gt;

&lt;h2 id=&quot;3-flat-hash-map-survey&quot;&gt;3. Flat Hash Map Survey&lt;/h2&gt;

&lt;p&gt;We’ve already introduced the principles of the flat hash map and concluded it’s a good fit for our scenario. Of course, we don’t need to reinvent the wheel, so the next step is to research available open-source flat hash map libraries. A search reveals many third-party libraries, and the article &lt;a href=&quot;https://martin.ankerl.com/2022/08/27/hashmap-bench-01/&quot;&gt;Comprehensive C++ Hashmap Benchmarks 2022&lt;/a&gt; tests several C++ hash map libraries, many of which are flat.&lt;/p&gt;

&lt;p&gt;Based on the summary of this article, I’ve tried &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashMap&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;4-abslflat_hash_map&quot;&gt;4. absl::flat_hash_map&lt;/h2&gt;

&lt;p&gt;The first reason for trying this one is that many articles recommend it, and it’s open-source from Google. Google supposedly uses it internally, which is quite impressive.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt; is part of the &lt;a href=&quot;https://github.com/abseil/abseil-cpp&quot;&gt;abseil-cpp&lt;/a&gt; library. To use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt;, you have to pull in a huge number of other dependencies, which seems to be a common issue with Google’s open-source libraries. You only want to use one small feature, but since it’s part of a larger library, you have to include a bunch of dependencies. Of course, this isn’t entirely Google’s fault; it’s a fundamental problem with C++. Unlike Python or Java, C++ lacks a cross-platform ABI, which means to use a C++ library, you generally have to recompile all of its dependencies. In the early days, C++ also lacked a central repository like Maven or PyPI (though things like Conan exist now, they’re not widely used), and compiling every library from scratch was a hassle. As a result, many people just used their own code or copied and modified others’ code, leading to fragmented branches that couldn’t be easily updated. A mature library has many dependencies. For example, you probably need logging for any code you write, so you need a dependency for that. You need to parse command-line arguments, another dependency. You need string manipulation, but the C++ standard library strings aren’t great, so you need to create your own or rely on one. You need regular expressions, XML/JSON/YAML parsing, etc. The C++ standard library lacks all of these. In contrast, in Python or Java, these features are either in the standard library or have converged on a few winning open-source libraries after several rounds of competition, leading to a more unified ecosystem. But C++ can’t converge, so every company has to build its own set of tools, and some companies even rebuild tools that the standard library already has because they don’t like them. This results in them open-sourcing almost an entire suite of libraries, making it impossible to separate a single small feature.&lt;/p&gt;

&lt;p&gt;There are also some very small libraries, usually header-only, that you can just copy directly into your project. Even with these, because they don’t have a package manager like PyPI or Maven, once you use them, you basically can’t (or don’t dare to) upgrade them. This makes them only suitable for libraries that will never be modified after their release. But even small libraries need bug fixes. You can’t just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff&lt;/code&gt; and apply patches yourself.&lt;/p&gt;

&lt;p&gt;Anyway, let’s get back to the main point. We first include &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt; in our code, which requires the full &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;abseil-cpp&lt;/code&gt; code. We’ve already got the code via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git submodule&lt;/code&gt;, so we just need to modify &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CMakeLists.txt&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;add_subdirectory(abseil-cpp)

target_link_libraries(bpe_train_updater_omp_v2 PRIVATE absl::flat_hash_map absl::hash)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, include the header file where you need to use it:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#include &quot;absl/container/flat_hash_map.h&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt; interface is similar to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt;, but it lacks the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bucket&lt;/code&gt; interface, so parallel traversal via buckets is not possible.&lt;/p&gt;

&lt;p&gt;The full code using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt; is in &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_omp_v2.cpp&quot;&gt;bpe_train_updater_omp_v2.cpp&lt;/a&gt;. The code is almost identical to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater.cpp&lt;/code&gt;, except that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt; has been replaced with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Previously, compiling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cppupdate&lt;/code&gt; took only tens of seconds. After introducing this dependency, it takes several minutes even with 8 threads.&lt;/p&gt;

&lt;h2 id=&quot;5-abslflat_hash_map-test&quot;&gt;5. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt; Test&lt;/h2&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program             &lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;7171/7856/9248&lt;/td&gt;
      &lt;td&gt;392/480/478&lt;/td&gt;
      &lt;td&gt;6779/7376/8770&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;907/908/955&lt;/td&gt;
      &lt;td&gt;514/503/554&lt;/td&gt;
      &lt;td&gt;391/403/400&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=32 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;2201/2392/2281&lt;/td&gt;
      &lt;td&gt;1931/2120/2010&lt;/td&gt;
      &lt;td&gt;269/272/270&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;We found that compared to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater&lt;/code&gt;, the total time for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_omp_v2&lt;/code&gt; dropped from around 8000 seconds to just over 2000 seconds. The max time is less than 300 seconds, which is more than 20 times faster than the 7000 seconds of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater&lt;/code&gt; and even faster than the 32-thread &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_omp_v7&lt;/code&gt;. This shows that the contiguous memory layout of the flat hash map is indeed well-suited for traversal. However, the update time increased from over 400 seconds to over 2000 seconds, which doesn’t seem to align with the benchmarks from other people online.&lt;/p&gt;

&lt;p&gt;As we mentioned before, the hash function for a hash map is crucial. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt; comes with its own hash function, which we need to test. We also have a very simple hash function that we implemented previously, which we can compare. The code using the simple hash is in &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_omp_v2_hash.cpp&quot;&gt;bpe_train_updater_omp_v2_hash.cpp&lt;/a&gt;, and the code using the native &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl&lt;/code&gt; hash is in &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_omp_v2_hash2.cpp&quot;&gt;bpe_train_updater_omp_v2_hash2.cpp&lt;/a&gt;. Additionally, we incorporated the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-=&lt;/code&gt; optimization into &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_opt_absl.cpp&quot;&gt;bpe_train_updater_opt_absl&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Using the native &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl&lt;/code&gt; hash is very simple; it’s used by default if you don’t specify a hash function:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;absl::flat_hash_map&amp;lt;std::pair&amp;lt;int, int&amp;gt;, int&amp;gt; pair_counts;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here are the test results:&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program             &lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;7171/7856/9248&lt;/td&gt;
      &lt;td&gt;392/480/478&lt;/td&gt;
      &lt;td&gt;6779/7376/8770&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;907/908/955&lt;/td&gt;
      &lt;td&gt;514/503/554&lt;/td&gt;
      &lt;td&gt;391/403/400&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=32 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;1268/1196/1215&lt;/td&gt;
      &lt;td&gt;548/473/481&lt;/td&gt;
      &lt;td&gt;719/723/734&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=16 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;2201/2392/2281&lt;/td&gt;
      &lt;td&gt;1931/2120/2010&lt;/td&gt;
      &lt;td&gt;269/272/270&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2&lt;/td&gt;
      &lt;td&gt;my hash&lt;/td&gt;
      &lt;td&gt;6276/6349/6613&lt;/td&gt;
      &lt;td&gt;6030/6104/6364&lt;/td&gt;
      &lt;td&gt;245/245/249&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash2&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1170/1074/1071&lt;/td&gt;
      &lt;td&gt;545/456/449&lt;/td&gt;
      &lt;td&gt;625/617/621&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_absl&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1072/1012/1022&lt;/td&gt;
      &lt;td&gt;423/378/384&lt;/td&gt;
      &lt;td&gt;648/633/637&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;When using a simple hash (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my hash&lt;/code&gt;), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt;’s max time is still around 200 seconds, but the update time increases to over 6000 seconds. However, when using the native &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl&lt;/code&gt; hash function, the max time increases to over 600 seconds (still far less than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater&lt;/code&gt;’s 7000+ seconds), but the update time is only around 500 seconds. The performance of this version is excellent, even surpassing the 16-thread speed of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_omp_v7&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpe_train_updater_opt_absl&lt;/code&gt; version, with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-=&lt;/code&gt; optimization, further reduces the update time to around 400 seconds.&lt;/p&gt;

&lt;h2 id=&quot;6-emhash8hashmap&quot;&gt;6. emhash8::HashMap&lt;/h2&gt;

&lt;p&gt;You can refer to &lt;a href=&quot;https://github.com/ktprime/emhash/issues/33#issuecomment-1636618464&quot;&gt;this issue&lt;/a&gt; for the principles behind &lt;strong&gt;emhash&lt;/strong&gt;. I haven’t researched it in depth myself, so interested readers can explore it on their own or ask questions on the project’s GitHub page.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ktprime/emhash&quot;&gt;emhash&lt;/a&gt; is much lighter than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt;. We only need to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8&lt;/code&gt;, so we just need to include the header files. We can do this by adding a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target_include_directories&lt;/code&gt; line to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CMakeLists.txt&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;target_include_directories(bpe_train_updater_emhash8 PUBLIC
    &quot;${PROJECT_SOURCE_DIR}/emhash&quot;
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The change is also very simple: just replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::unordered_map&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashMap&lt;/code&gt;. The full code is in &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_emhash8.cpp&quot;&gt;bpe_train_updater_emhash8.cpp&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;7-emhash8hashmap-test-results&quot;&gt;7. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashMap&lt;/code&gt; Test Results&lt;/h2&gt;

&lt;p&gt;For comparison, we also implemented an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-=&lt;/code&gt; optimized version, &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_opt_emhash8.cpp&quot;&gt;bpe_train_updater_opt_emhash8.cpp&lt;/a&gt;, and an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-=&lt;/code&gt; optimized version with a custom hash function, &lt;a href=&quot;https://github.com/fancyerii/assignment1-basics-bpe/blob/main/cppupdate/bpe_train_updater_opt_emhash8_hash.cpp&quot;&gt;bpe_train_updater_opt_emhash8_hash.cpp&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here are the results:&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;program             &lt;/td&gt;
      &lt;td&gt;hash function&lt;/td&gt;
      &lt;td&gt;total time(sec)&lt;/td&gt;
      &lt;td&gt;update time(sec)&lt;/td&gt;
      &lt;td&gt;max time(sec)&lt;/td&gt;
      &lt;td&gt;other&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;7171/7856/9248&lt;/td&gt;
      &lt;td&gt;392/480/478&lt;/td&gt;
      &lt;td&gt;6779/7376/8770&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;907/908/955&lt;/td&gt;
      &lt;td&gt;514/503/554&lt;/td&gt;
      &lt;td&gt;391/403/400&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=32 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v7&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;1268/1196/1215&lt;/td&gt;
      &lt;td&gt;548/473/481&lt;/td&gt;
      &lt;td&gt;719/723/734&lt;/td&gt;
      &lt;td&gt;export OMP_NUM_THREADS=16 export OMP_SCHEDULE=”dynamic,1000”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;2201/2392/2281&lt;/td&gt;
      &lt;td&gt;1931/2120/2010&lt;/td&gt;
      &lt;td&gt;269/272/270&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2&lt;/td&gt;
      &lt;td&gt;my hash&lt;/td&gt;
      &lt;td&gt;6276/6349/6613&lt;/td&gt;
      &lt;td&gt;6030/6104/6364&lt;/td&gt;
      &lt;td&gt;245/245/249&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_omp_v2_hash2&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1170/1074/1071&lt;/td&gt;
      &lt;td&gt;545/456/449&lt;/td&gt;
      &lt;td&gt;625/617/621&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_absl&lt;/td&gt;
      &lt;td&gt;Absl hash&lt;/td&gt;
      &lt;td&gt;1072/1012/1022&lt;/td&gt;
      &lt;td&gt;423/378/384&lt;/td&gt;
      &lt;td&gt;648/633/637&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_emhash8&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;479/485/485&lt;/td&gt;
      &lt;td&gt;398/401/401&lt;/td&gt;
      &lt;td&gt;80/83/83&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_emhash8&lt;/td&gt;
      &lt;td&gt;Boost hash&lt;/td&gt;
      &lt;td&gt;469/474/479&lt;/td&gt;
      &lt;td&gt;389/395/399&lt;/td&gt;
      &lt;td&gt;79/78/79&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;bpe_train_updater_opt_emhash8_hash&lt;/td&gt;
      &lt;td&gt;my hash&lt;/td&gt;
      &lt;td&gt;2316/1951/1983&lt;/td&gt;
      &lt;td&gt;2250/1888/1918&lt;/td&gt;
      &lt;td&gt;66/63/64&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The speed of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emhash8::HashMap&lt;/code&gt; is even faster than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;absl::flat_hash_map&lt;/code&gt;, with a total time of less than 500 seconds. The max time is even less than 100 seconds. Using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-=&lt;/code&gt; optimization shaves off another ten seconds or so. At the same time, we see that performance also degrades if we use a simple hash function.&lt;/p&gt;

&lt;h2 id=&quot;8-summary&quot;&gt;8. Summary&lt;/h2&gt;

&lt;p&gt;By using a flat hash map, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max&lt;/code&gt; time was drastically reduced, which proves that the flat, contiguous memory structure is indeed suitable for our scenario. As a result, we achieved a very high speed without even needing parallel optimization. The lesson here is that optimizing data structures and algorithms can sometimes yield better results than parallelization. We’ve accomplished the same task with fewer resources and at a faster speed.&lt;/p&gt;

&lt;p&gt;With this, our work on optimizing the second step with C++ is temporarily concluded. Next, we will return to Python to continue our optimizations.&lt;/p&gt;

&lt;h2 id=&quot;full-series&quot;&gt;Full Series&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/05/bpe-trainer-0_en/&quot;&gt;Part 0: Introduction&lt;/a&gt; Introduces the basic BPE training algorithm and related tasks, as well as the development environment.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/07/bpe-trainer-1_en/&quot;&gt;Part 1: The Simplest Implementation&lt;/a&gt; The simplest implementation of BPE training.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/08/bpe-trainer-2_en/&quot;&gt;Part 2: Optimized Algorithm&lt;/a&gt; Implements incremental updates for pair_counts.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/09/bpe-trainer-3_en/&quot;&gt;Part 3: Parallel Tokenization and Frequency Counting&lt;/a&gt; Uses multiprocessing to implement a multi-process parallel algorithm.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/10/bpe-trainer-4_en/&quot;&gt;Part 4: A Failed Parallel Optimization&lt;/a&gt; An attempt to parallelize the max pair calculation using multiple processes.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/12/bpe-trainer-5_en/&quot;&gt;Part 5: Implementing the Merge Algorithm in C++&lt;/a&gt; Implements a C++ merge algorithm equivalent to the Python version, and compares two ways of iterating through std::unordered_map.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/15/bpe-trainer-6_en/&quot;&gt;Part 6: Parallelizing the Max Pair Search with OpenMP&lt;/a&gt; Uses OpenMP to find the max pair in pair_counts in parallel.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/18/bpe-trainer-7_en/&quot;&gt;Part 7: Using Flat Hashmap to Replace std::unordered_map&lt;/a&gt; Uses flat hashmap to replace std::unordered_map.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/19/bpe-trainer-8_en/&quot;&gt;Part 8: Implementing Fine-Grained Updates&lt;/a&gt; Implements a fine-grained update algorithm for pair_counts using an inverted index.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/21/bpe-trainer-9_en/&quot;&gt;Part 9: Using a Heap to Find the Max Pair&lt;/a&gt; Uses a heap to find the max pair and improve performance.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/24/bpe-trainer-10_en/&quot;&gt;Part 10: Using Cython and PyPy for Acceleration&lt;/a&gt; Uses Cython and PyPy to accelerate Python code.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/2025/09/25/bpe-trainer-11_en/&quot;&gt;Part 11: Wrapping C++ Code with Cython&lt;/a&gt; Wraps C++ code using Cython.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 18 Sep 2025 00:00:00 +0000</pubDate>
        <link>http://fancyerii.github.io/2025/09/18/bpe-trainer-7_en/</link>
        <guid isPermaLink="true">http://fancyerii.github.io/2025/09/18/bpe-trainer-7_en/</guid>
        
        <category>cs336</category>
        
        <category>bpe tokenizer</category>
        
        
      </item>
    
  </channel>
</rss>
