The Third Wave of Parallelism: Optimizing a 902K-Sample Tokenization Pipeline
A Single Line That Represents an Iterative Optimization Journey
In the middle of a sprawling machine learning engineering session spanning dozens of messages, one brief assistant message stands out for what it represents rather than what it says:
Message 7717: "Now let me rewrite the tokenization to use multiprocessing for the CPU-bound tokenization too, and parallelize the S3 upload:"
Followed by a single file edit. That's it — a two-sentence declaration and an edit action. Yet this message is the culmination of a rapid, three-stage optimization process driven by user feedback, practical constraints, and the relentless pressure of processing nearly a million samples. It captures a moment where the assistant, having already parallelized one bottleneck, realizes that another bottleneck lurks deeper in the pipeline and must be addressed before the system can run at scale.
The Context: A Pipeline Under Pressure
To understand why this message matters, we need to step back. The broader project is training a DFlash speculative decoding drafter for Qwen3.6-27B, a 27-billion-parameter language model. The training data consists of 902,087 completions — model-generated responses with full thinking traces — produced by running Qwen3.6-27B on a B200 NVL node. These completions are stored as 1,805 JSONL files in S3, totaling 7.25 GB of data and 1.64 billion output tokens.
The pipeline has three phases:
- Phase 1 (Tokenization): Download the JSONL files from S3, apply the Qwen3.6 chat template with thinking tokens, generate loss masks, and save the result as an Arrow dataset back to S3.
- Phase 2 (Online Extraction): Load the tokenized data and run it through the target model (Qwen3.6-27B) on GPU, extracting hidden states from specific layers.
- Phase 3 (Training): Feed those hidden states into the DFlash drafter model for training. The critical insight driving Phase 1 is that tokenization is purely CPU-bound — it involves text processing, template application, and tokenization via the Hugging Face tokenizer, with no GPU required. This means it can be done on any machine with enough CPU cores and memory, freeing the precious GPU-equipped machines for the actual training. But 902,087 samples is a lot of text. At the time of this message, the assistant had already written the tokenization script ([msg 7699]), syntax-checked it ([msg 7702]), and was now iterating on its performance after user feedback.
The Three Waves of Parallelization
The optimization of the tokenization script happened in three distinct waves, each triggered by user input or the assistant's own recognition of a bottleneck.
Wave 1: Serial Download (The Original Sin)
The initial version of tokenize_completions.py downloaded all 1,805 JSONL files one at a time. In a serial loop. For 1,805 files. This was the obvious first implementation — get it working, then optimize. The assistant's reasoning at this stage (visible in [msg 7696]) was focused on correctness: implementing the chat template correctly, generating proper loss masks, and ensuring the Arrow dataset schema was right. Performance was a secondary concern.
Wave 2: Thread Pool for Download (User-Triggered)
The user immediately spotted the problem. In [msg 7706], they asked: "Are we parallelising the download? 2k files ideally we can download 10-50 at a time." This is a clear signal: the user understands that S3 download is I/O-bound and benefits from concurrent connections. The assistant responded in [msg 7708] by adding a ThreadPoolExecutor with 32 workers, each creating its own boto3 client (since S3 clients are not thread-safe). A quick bug fix followed in [msg 7709] when the assistant noticed a stray threading.atomic reference that didn't exist. The assistant estimated this would reduce download time from 30+ minutes to "a couple minutes" ([msg 7711]).
Wave 3: Multiprocessing for Tokenization + Parallel S3 Upload (The Subject Message)
The user then pushed further in [msg 7712]: "Tokenize here and put tokenized in S3 too. Use high parallelism too." Two directives here: upload the results to S3 (which wasn't in the original plan), and maximize parallelism. The assistant initially installed dependencies and verified imports (<msg id=7713-7714>), then read the script to assess the situation (<msg id=7715-7716>).
This brings us to the subject message ([msg 7717]). The assistant recognizes two remaining bottlenecks:
- Tokenization is CPU-bound and serial. Even though the download is now parallel, the actual tokenization — applying the chat template, running the tokenizer, generating loss masks — happens sample by sample. With 902,087 samples, this would take hours on a single CPU core.
- S3 upload is also serial. After tokenization, the Arrow shards need to be uploaded to S3. If this happens sequentially, it adds more wall-clock time. The decision is to use multiprocessing for the CPU-bound tokenization work (since Python's GIL makes threading ineffective for CPU-bound tasks) and to parallelize the S3 upload as well (likely using a thread pool, since S3 upload is I/O-bound).
The Assumptions Behind the Decision
This message encodes several assumptions, some explicit and some implicit:
Assumption 1: Multiprocessing is the right tool for CPU-bound tokenization. This is a sound engineering judgment. Python's multiprocessing module bypasses the GIL by spawning separate processes, each with its own Python interpreter and memory space. For tokenization — which involves string processing, regex operations, and calling into C extensions (the Hugging Face tokenizer) — this is appropriate. The overhead of serializing/deserializing data between processes is justified by the parallelism gains.
Assumption 2: The tokenizer is safe to use across multiple processes. Hugging Face tokenizers are generally thread-safe and process-safe, but there can be edge cases with shared state or lazy initialization. The assistant assumes that instantiating separate tokenizer instances in each worker process will work correctly.
Assumption 3: The Arrow dataset library can handle concurrent writes. The script needs to merge results from multiple worker processes into a single Arrow dataset. This requires careful coordination — either writing to separate shards and concatenating, or using a shared lock. The assistant's edit presumably handles this.
Assumption 4: S3 upload parallelism won't overwhelm the endpoint. Uploading 47 Arrow shards (the final count, as revealed in [chunk 44.1]) concurrently to S3 could potentially trigger rate limiting or connection throttling. The assistant assumes the S3 endpoint can handle the concurrent traffic.
Assumption 5: The machine has enough CPU cores. The assistant was running on a machine that was previously used for GPU work (it had PyTorch installed at some point, though it was later removed). The assumption is that there are sufficient cores to make multiprocessing worthwhile — likely 12+ cores based on the --tokenize-workers 12 flag used in the subsequent run ([msg 7721]).
What the Edit Actually Changed
While the subject message itself doesn't show the diff (it says "Edit applied successfully"), we can infer what changed by comparing the before and after states. The script originally had:
- A serial loop over downloaded JSONL files, tokenizing one at a time
- A serial upload at the end (or no upload at all, since that was added in response to the user's request) The edit transformed this into:
- A
multiprocessing.Pool(or similar) that distributes tokenization work across N worker processes - A parallel upload mechanism (likely a
ThreadPoolExecutor) for the resulting Arrow shards - Coordination logic to merge results from parallel workers The subsequent run in [msg 7721] used
--tokenize-workers 12, confirming that 12 parallel tokenization processes were configured.
The Immediate Aftermath: A Practical Bug
The very next message ([msg 7718]) reveals that the edit introduced a bug: the script was missing import time. The assistant caught this by re-reading the file and fixed it in [msg 7719]. Then, when actually running the script in [msg 7721], a different error surfaced: ModuleNotFoundError: No module named 'torch'. The script had an unnecessary import torch statement that wasn't needed for tokenization (the tokenizer doesn't require PyTorch), but the import was left over from an earlier version. The assistant removed it in [msg 7722].
These post-edit bugs are instructive. They reveal the tension between rapid iteration and correctness. The assistant was moving fast — responding to user requests, making edits, testing, fixing — and the multiprocessing edit introduced two issues that had to be caught and corrected in subsequent rounds. This is normal in interactive coding sessions, but it highlights that even well-reasoned edits can have unintended consequences.
The Result: 902K Samples in 6.5 Minutes
The payoff of this optimization was substantial. According to the chunk summary for segment 44 ([chunk 44.1]), the tokenization completed successfully, processing all 902,087 samples in just 6.5 minutes. It produced 1.87 billion tokens (87.5% of which were loss tokens — meaning the model is trained to predict them), organized into 47 Arrow shards. This is a 5.75× improvement in data quality compared to the old prompt-only dataset, because the new dataset includes the model's full thinking traces rather than just the prompts.
The 6.5-minute runtime for 902K samples implies a throughput of roughly 2,300 samples per second, or about 4.8 million tokens per second. This is only achievable through aggressive parallelism — 12 concurrent tokenization processes, each presumably handling a batch of files.
The Broader Significance
This message, for all its brevity, illustrates several important patterns in machine learning engineering:
1. Pipeline optimization is iterative and user-driven. The assistant didn't anticipate the need for parallelism in the first implementation. It took user feedback ("Are we parallelising the download?") to trigger the first optimization, and a further push ("Use high parallelism too") to trigger the second. This is a realistic picture of how engineering work proceeds: you build the simplest correct version first, then optimize based on observed bottlenecks and stakeholder input.
2. Bottlenecks shift as you optimize. The initial bottleneck was download (I/O-bound). Fixing that revealed the next bottleneck: tokenization (CPU-bound). Fixing that revealed the next: S3 upload (I/O-bound again). Each optimization layer exposes the next constraint in the pipeline. This is the classic "bottleneck shifting" pattern in systems engineering.
3. CPU-bound work in Python requires multiprocessing. The assistant's decision to use multiprocessing rather than threading for tokenization reflects a correct understanding of Python's GIL limitations. For I/O-bound work (download, upload), threading works fine because the GIL is released during I/O waits. For CPU-bound work (tokenization), you need separate processes to achieve parallelism.
4. The gap between "works on my machine" and "works at scale." The initial serial implementation was correct but impractical for 902K samples. The optimizations — while seemingly small in the code — were what made the pipeline actually usable. Without them, tokenization would have taken hours instead of minutes, potentially blocking the entire training schedule.
5. Small edits can have large impacts. The subject message is just one line of text and one file edit. Yet it represents the difference between a pipeline that takes hours and one that takes minutes. In ML engineering, where data pipelines often process millions of samples, these constant-factor optimizations compound dramatically.
Conclusion
Message 7717 is a study in brevity masking complexity. On the surface, it's a simple declaration of intent followed by a file edit. But in context, it's the third wave of a rapid optimization cycle, driven by user feedback, grounded in systems engineering principles (GIL, I/O vs. CPU bound, multiprocessing vs. threading), and aimed at making a 902K-sample tokenization pipeline practical. The 6.5-minute runtime that resulted — processing 1.87 billion tokens across 47 Arrow shards — was only possible because the assistant recognized that CPU-bound tokenization needed multiprocessing, not just threading, and that S3 upload needed parallelization too.
The message also reveals the messy reality of interactive coding: the edit introduced bugs (missing import, unnecessary torch import) that had to be fixed in subsequent rounds. But that's how real engineering works — iterate, test, fix, repeat. The final result was a pipeline that worked correctly and efficiently, transforming 902,087 raw completions into a clean, tokenized training dataset ready for the GPU-intensive phases to come.