feat: wire in the thread pool - #2331
Conversation
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
There was a problem hiding this comment.
Pull request overview
This PR introduces a streaming/parallel encoding path for the experimental PipelineTokenizer, wiring it to the shared thread pool and planning work units by splitting inputs on special tokens to enable parallel execution.
Changes:
- Refactors
PipelineTokenizerto beClonevia anArc-backed inner struct, enabling sharing across worker threads. - Adds a new streaming
EncodeHandlemode backed by aJobwork-queue with atomic coordination and per-unit outputs. - Updates special-token segmentation to track input offsets (
Segment::Text { text, input_offset }) for range-based unit encoding.
Suppressed comments (4)
tokenizers/tk-encode/src/tokenizer/pipeline.rs:1143
- Parallel path runs
encode_genericatSTAGE_POSTPROCESSfor each unit. When inputs are split around special tokens, this applies the post-processor prefix/suffix per unit (and can also place prefix/suffix after leading special tokens or before trailing special tokens), producing different output than the serial encode.
let Plan { units, outputs } = self.plan_work(&inputs);
if units.len() < 2 {
return EncodeHandle::blocking(self.encode_serial(inputs, add_special_tokens));
}
tokenizers/tk-encode/src/tokenizer/pipeline.rs:1181
encode_serialcurrently panics onInput::Pairviatodo!("handle input pairs"). This is a public API surface (encodeaccepts tuples viaInto<Inputs>), so it should return anErrrather than panic.
Inputs::Single(seq) => {
let Input::Single(seq) = seq else {
todo!("handle input pairs")
};
tokenizers/tk-encode/src/tokenizer/pipeline.rs:1191
encode_serialpanics onInput::Pairentries inside a batch. This makes mixed batches a runtime panic instead of a per-item error.
for seq in batch {
let Input::Single(seq) = seq else {
todo!("handle input pairs")
};
tokenizers/tk-encode/src/tokenizer/pipeline.rs:1087
plan_workpanics onInput::Pairviatodo!("handle pair input"), so passing a pair can crash before falling back to serial. Consider planning a single unit that will deterministically return anErrin the worker, matching the serial behavior.
for (seq_idx, input) in inputs.into_iter().enumerate() {
let Input::Single(input) = input else {
todo!("handle pair input")
};
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| pub fn encode(&self, inputs: impl Into<Inputs>, add_special_tokens: bool) -> EncodeHandle { | ||
| let inputs = inputs.into(); | ||
|
|
||
| if inputs.size_bytes() < PARALLEL_MIN_BYTES { | ||
| EncodeHandle::blocking(self.encode_serial(inputs, add_special_tokens)) | ||
| } else { | ||
| let Some(pool) = pool() else { | ||
| // XXX: unable to get a pool, handle, reverting to single threaded | ||
| return EncodeHandle::blocking(self.encode_serial(inputs, add_special_tokens)); |
SBrandeis
left a comment
There was a problem hiding this comment.
first pass of review 🥵
| use std::{borrow::Cow, convert::TryFrom}; | ||
|
|
||
| use atomsplit::classify::classify; | ||
| use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; |
There was a problem hiding this comment.
Unused imports I think, don't forget to run clippy and cargo fmt!
| Single(String), | ||
| Pair(String, String), | ||
| Batch(Vec<String>), | ||
| PairBatch(Vec<(String, String)>), | ||
| } |
There was a problem hiding this comment.
Pre-existing to this PR, but does this require to copy the string from Python to Rust?
There was a problem hiding this comment.
You can make it 0-copy from a python string all the way to the argument in rust, although somewhere in the process you need to materialise the string to utf8 from the python side which will necessarily copy the string. We simply make access from it's utf8 storage 0-copy via some unsafe, but we can hold a reference to the object making it safe.
| let unit_results = &self.outputs[seq]; | ||
| if unit_results.len() == 1 { | ||
| return unit_results[0].take().unwrap_or_else(|| { | ||
| unreachable!("failed to take the unit's result when we expect it to be present") |
There was a problem hiding this comment.
That can happen if you try to take the result before the work producing it is completed, right?
Should we return an Error here instead of panicking?
There was a problem hiding this comment.
It should not be possible for this value to be None. If we call take_result before the work has been completed, this is a bug. Imo it shouldn't be an error for the user to handle because it is unrecoverable anyways and hard panic is valid behaviour.
| // just empty spinning, we actually do useful work | ||
| // it's also very simple to implement | ||
| if !job.encode_unit() { | ||
| std::hint::spin_loop(); |
There was a problem hiding this comment.
from the fn's doc:
core::hint
pub fn spin_loop()
Emits a machine instruction to signal the processor that it is running in
a busy-wait spin-loop ("spin lock").Upon receiving the spin-loop signal the processor can optimize its behavior by,
for example, saving power or switching hyper-threads.This function is different from
thread::yield_nowwhich directly
yields to the system's scheduler, whereasspin_loopdoes not interact
with the operating system.A common use case for
spin_loopis implementing bounded optimistic
spinning in a CAS loop in synchronization primitives. To avoid problems
like priority inversion, it is strongly recommended that the spin loop is
terminated after a finite amount of iterations and an appropriate blocking
syscall is made.Note: On platforms that do not support receiving spin-loop hints this
function does not do anything at all.
|
|
||
| struct Unit { | ||
| /// Sequence index in the [`Inputs`] batch | ||
| seq: usize, |
There was a problem hiding this comment.
🙈
| seq: usize, | |
| sequence_idx: usize, |
There was a problem hiding this comment.
I fear it makes the Unit::idx less clear afterwards actually, I think I'd rather leave it as seq!
|
|
||
| const PARALLEL_MIN_BYTES: usize = 8 * 1024; | ||
|
|
||
| struct Unit { |
There was a problem hiding this comment.
naming suggestion, feel free to disregard
| struct Unit { | |
| struct WorkUnit { |
There was a problem hiding this comment.
Well, it's a unit to work on, not a unit of work technically 🤓
Signed-off-by: Luc Georges <luc.sydney.georges@gmail.com>
Wiring in the thread pool.
For now we only parallelise on special tokens, the intra-seq splitting will come in a subsequent PR. I also need to handle
Pairs properly, but also for another PR.Left to do in this PR: