This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-mosaic.git


The following commit(s) were added to refs/heads/main by this push:
     new 57bff28  perf(core): speed up schema BPE construction (#89)
57bff28 is described below

commit 57bff28e5ca33d212baeb144f052001141b2c929
Author: jianguotian <[email protected]>
AuthorDate: Sun Sep 20 20:03:30 2026 +0800

    perf(core): speed up schema BPE construction (#89)
---
 core/src/bpe.rs    | 262 +++++++++++++++++++++++++++++++++++++++++++++++------
 core/src/schema.rs |  11 +--
 2 files changed, 237 insertions(+), 36 deletions(-)

diff --git a/core/src/bpe.rs b/core/src/bpe.rs
index d2d8be8..f334e1e 100644
--- a/core/src/bpe.rs
+++ b/core/src/bpe.rs
@@ -17,37 +17,41 @@
 
 use std::collections::HashMap;
 
-const TOKEN_BASE: usize = 0x80;
+const TOKEN_BASE: u8 = 0x80;
 const MAX_RULES: usize = 128;
+const PAIR_TABLE_SIZE: usize = 1 << 16;
+const DENSE_PAIR_THRESHOLD: usize = 4_096;
 
 pub fn is_ascii_only(names: &[&[u8]]) -> bool {
     names.iter().all(|name| name.iter().all(|&b| b & 0x80 == 0))
 }
 
 pub fn build_vocabulary(names: &[&[u8]]) -> Vec<[u8; 2]> {
-    let mut tokens: Vec<Vec<u16>> = names
-        .iter()
-        .map(|name| name.iter().map(|&b| b as u16).collect())
-        .collect();
+    build_vocabulary_and_encode(names).0
+}
+
+pub(crate) fn build_vocabulary_and_encode(names: &[&[u8]]) -> (Vec<[u8; 2]>, 
Vec<Vec<u8>>) {
+    let mut tokens: Vec<Vec<u8>> = names.iter().map(|name| 
name.to_vec()).collect();
 
     let mut rules = Vec::new();
+    let use_dense_counter = tokens
+        .iter()
+        .map(|seq| seq.len().saturating_sub(1))
+        .sum::<usize>()
+        >= DENSE_PAIR_THRESHOLD;
+    let mut pair_counts = use_dense_counter.then(|| vec![0u32; 
PAIR_TABLE_SIZE]);
+    let mut touched_pairs = Vec::new();
 
     for _ in 0..MAX_RULES {
-        let mut pair_counts: HashMap<u32, u32> = HashMap::new();
-        for seq in &tokens {
-            for w in seq.windows(2) {
-                let pair = (w[0] as u32) << 16 | w[1] as u32;
-                *pair_counts.entry(pair).or_default() += 1;
-            }
-        }
-
-        let best = pair_counts.iter().max_by_key(|&(&k, &v)| (v, k));
+        let best = if let Some(pair_counts) = &mut pair_counts {
+            most_frequent_pair_dense(&tokens, pair_counts, &mut touched_pairs)
+        } else {
+            most_frequent_pair_hashmap(&tokens)
+        };
         match best {
-            Some((&pair, &count)) if count > 1 => {
-                let left = (pair >> 16) as u16;
-                let right = (pair & 0xFFFF) as u16;
-                let new_token = (TOKEN_BASE + rules.len()) as u16;
-                rules.push([left as u8, right as u8]);
+            Some((left, right, count)) if count > 1 => {
+                let new_token = TOKEN_BASE + rules.len() as u8;
+                rules.push([left, right]);
 
                 for seq in &mut tokens {
                     replace_pair(seq, left, right, new_token);
@@ -57,10 +61,55 @@ pub fn build_vocabulary(names: &[&[u8]]) -> Vec<[u8; 2]> {
         }
     }
 
-    rules
+    (rules, tokens)
+}
+
+fn most_frequent_pair_dense(
+    tokens: &[Vec<u8>],
+    pair_counts: &mut [u32],
+    touched_pairs: &mut Vec<u16>,
+) -> Option<(u8, u8, u32)> {
+    debug_assert_eq!(pair_counts.len(), PAIR_TABLE_SIZE);
+    for pair in touched_pairs.drain(..) {
+        pair_counts[pair as usize] = 0;
+    }
+
+    let mut best_pair = 0usize;
+    let mut best_count = 0u32;
+    for seq in tokens {
+        for pair in seq.windows(2) {
+            let pair = ((pair[0] as usize) << 8) | pair[1] as usize;
+            if pair_counts[pair] == 0 {
+                touched_pairs.push(pair as u16);
+            }
+            pair_counts[pair] += 1;
+            let count = pair_counts[pair];
+            if count > best_count || (count == best_count && pair > best_pair) 
{
+                best_pair = pair;
+                best_count = count;
+            }
+        }
+    }
+
+    (best_count != 0).then_some(((best_pair >> 8) as u8, (best_pair & 0xff) as 
u8, best_count))
 }
 
-fn replace_pair(seq: &mut Vec<u16>, left: u16, right: u16, new_token: u16) {
+fn most_frequent_pair_hashmap(tokens: &[Vec<u8>]) -> Option<(u8, u8, u32)> {
+    let mut pair_counts: HashMap<u16, u32> = HashMap::new();
+    for seq in tokens {
+        for pair in seq.windows(2) {
+            let pair = (pair[0] as u16) << 8 | pair[1] as u16;
+            *pair_counts.entry(pair).or_default() += 1;
+        }
+    }
+
+    pair_counts
+        .into_iter()
+        .max_by_key(|&(pair, count)| (count, pair))
+        .map(|(pair, count)| ((pair >> 8) as u8, pair as u8, count))
+}
+
+fn replace_pair(seq: &mut Vec<u8>, left: u8, right: u8, new_token: u8) {
     let mut i = 0;
     let mut out = 0;
     while i < seq.len() {
@@ -77,16 +126,16 @@ fn replace_pair(seq: &mut Vec<u16>, left: u16, right: u16, 
new_token: u16) {
 }
 
 pub fn encode(name: &[u8], rules: &[[u8; 2]]) -> Vec<u8> {
-    let mut tokens: Vec<u16> = name.iter().map(|&b| b as u16).collect();
+    let mut tokens = name.to_vec();
 
     for (r, rule) in rules.iter().enumerate() {
-        let left = rule[0] as u16;
-        let right = rule[1] as u16;
-        let new_token = (TOKEN_BASE + r) as u16;
+        let left = rule[0];
+        let right = rule[1];
+        let new_token = TOKEN_BASE + r as u8;
         replace_pair(&mut tokens, left, right, new_token);
     }
 
-    tokens.iter().map(|&t| t as u8).collect()
+    tokens
 }
 
 pub fn decode(encoded: &[u8], rules: &[[u8; 2]]) -> Vec<u8> {
@@ -98,10 +147,10 @@ pub fn decode(encoded: &[u8], rules: &[[u8; 2]]) -> 
Vec<u8> {
 }
 
 fn expand(token: usize, rules: &[[u8; 2]], out: &mut Vec<u8>) {
-    if token < TOKEN_BASE {
+    if token < TOKEN_BASE as usize {
         out.push(token as u8);
     } else {
-        let idx = token - TOKEN_BASE;
+        let idx = token - TOKEN_BASE as usize;
         expand(rules[idx][0] as usize, rules, out);
         expand(rules[idx][1] as usize, rules, out);
     }
@@ -111,6 +160,81 @@ fn expand(token: usize, rules: &[[u8; 2]], out: &mut 
Vec<u8>) {
 mod tests {
     use super::*;
 
+    const LEGACY_TOKEN_BASE: u8 = 0x80;
+    const LEGACY_MAX_RULES: usize = 128;
+
+    fn reference_replace_pair(seq: &mut Vec<u16>, left: u16, right: u16, 
new_token: u16) {
+        let mut i = 0;
+        let mut out = 0;
+        while i < seq.len() {
+            if i + 1 < seq.len() && seq[i] == left && seq[i + 1] == right {
+                seq[out] = new_token;
+                i += 2;
+            } else {
+                seq[out] = seq[i];
+                i += 1;
+            }
+            out += 1;
+        }
+        seq.truncate(out);
+    }
+
+    fn reference_build_vocabulary(names: &[&[u8]]) -> Vec<[u8; 2]> {
+        let mut tokens: Vec<Vec<u16>> = names
+            .iter()
+            .map(|name| name.iter().map(|&b| b as u16).collect())
+            .collect();
+        let mut rules = Vec::new();
+
+        for _ in 0..LEGACY_MAX_RULES {
+            let mut pair_counts: HashMap<u32, u32> = HashMap::new();
+            for seq in &tokens {
+                for pair in seq.windows(2) {
+                    let pair = (pair[0] as u32) << 16 | pair[1] as u32;
+                    *pair_counts.entry(pair).or_default() += 1;
+                }
+            }
+
+            match pair_counts
+                .iter()
+                .max_by_key(|&(&pair, &count)| (count, pair))
+            {
+                Some((&pair, &count)) if count > 1 => {
+                    let left = (pair >> 16) as u16;
+                    let right = pair as u16;
+                    let new_token = LEGACY_TOKEN_BASE as u16 + rules.len() as 
u16;
+                    rules.push([left as u8, right as u8]);
+                    for seq in &mut tokens {
+                        reference_replace_pair(seq, left, right, new_token);
+                    }
+                }
+                _ => break,
+            }
+        }
+
+        rules
+    }
+
+    fn legacy_decode(encoded: &[u8], rules: &[[u8; 2]]) -> Vec<u8> {
+        let mut out = Vec::with_capacity(encoded.len() * 2);
+        for &token in encoded {
+            legacy_expand(token as usize, rules, &mut out);
+        }
+        out
+    }
+
+    fn legacy_expand(token: usize, rules: &[[u8; 2]], out: &mut Vec<u8>) {
+        if token < LEGACY_TOKEN_BASE as usize {
+            out.push(token as u8);
+        } else {
+            let rule = rules
+                .get(token - LEGACY_TOKEN_BASE as usize)
+                .expect("encoded token must reference a frozen legacy rule");
+            legacy_expand(rule[0] as usize, rules, out);
+            legacy_expand(rule[1] as usize, rules, out);
+        }
+    }
+
     #[test]
     fn test_bpe_encode() {
         let names: Vec<&[u8]> = vec![
@@ -128,9 +252,91 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_build_vocabulary_and_encode_matches_separate_encoding() {
+        let mut owned_names = vec![b"aaaaa".to_vec()];
+        owned_names.extend((0..128).map(|i| {
+            format!(
+                
"vehicle_data_collection_powertrain_group_{:03}_signal_measurement_{i:05}_value",
+                i % 17
+            )
+            .into_bytes()
+        }));
+        let names: Vec<&[u8]> = 
owned_names.iter().map(Vec::as_slice).collect();
+
+        let (rules, encoded) = build_vocabulary_and_encode(&names);
+        assert_eq!(rules, reference_build_vocabulary(&names));
+        assert_eq!(
+            encoded,
+            names
+                .iter()
+                .map(|name| encode(name, &rules))
+                .collect::<Vec<_>>()
+        );
+    }
+
+    #[test]
+    fn test_full_legacy_token_space_round_trips() {
+        let owned_names: Vec<Vec<u8>> = (0..4096)
+            .map(|i| {
+                format!("prefix_{:04}_{}_{}_{}_suffix", i % 257, i, i % 97, i 
% 31).into_bytes()
+            })
+            .collect();
+        let names: Vec<&[u8]> = 
owned_names.iter().map(Vec::as_slice).collect();
+
+        let (rules, encoded) = build_vocabulary_and_encode(&names);
+
+        assert_eq!(rules.len(), LEGACY_MAX_RULES);
+        assert_eq!(rules, reference_build_vocabulary(&names));
+        assert!(encoded.iter().flatten().any(|&token| token == 0xff));
+        for (original, encoded_name) in names.iter().zip(&encoded) {
+            assert_eq!(legacy_decode(encoded_name, &rules), *original);
+            assert_eq!(decode(encoded_name, &rules), *original);
+        }
+    }
+
     #[test]
     fn test_ascii_only() {
         assert!(is_ascii_only(&[b"hello", b"world"]));
         assert!(!is_ascii_only(&[b"hello", &[0x80, 0x81]]));
     }
+
+    #[test]
+    fn test_dense_pair_counter_reuses_storage_and_preserves_tie_breaking() {
+        let mut counts = vec![0; 1 << 16];
+        let mut touched = Vec::new();
+
+        let first = vec![vec![1, 2, 1, 3]];
+        assert_eq!(
+            most_frequent_pair_dense(&first, &mut counts, &mut touched),
+            Some((2, 1, 1))
+        );
+
+        let second = vec![vec![255, 254, 255, 254]];
+        assert_eq!(
+            most_frequent_pair_dense(&second, &mut counts, &mut touched),
+            Some((255, 254, 2))
+        );
+    }
+
+    #[test]
+    fn test_dense_pair_counter_matches_hashmap_reference() {
+        let mut state = 1u64;
+        for _ in 0..32 {
+            let mut names = Vec::new();
+            for _ in 0..256 {
+                let len = 4 + (state as usize % 28);
+                let mut name = Vec::with_capacity(len);
+                for _ in 0..len {
+                    state = state
+                        .wrapping_mul(6_364_136_223_846_793_005)
+                        .wrapping_add(1);
+                    name.push(b'a' + ((state >> 32) % 26) as u8);
+                }
+                names.push(name);
+            }
+            let refs: Vec<&[u8]> = names.iter().map(Vec::as_slice).collect();
+            assert_eq!(build_vocabulary(&refs), 
reference_build_vocabulary(&refs));
+        }
+    }
 }
diff --git a/core/src/schema.rs b/core/src/schema.rs
index e5c6d78..ecfc6d1 100644
--- a/core/src/schema.rs
+++ b/core/src/schema.rs
@@ -240,12 +240,11 @@ impl MosaicSchema {
     pub fn serialize(&self) -> Vec<u8> {
         let num_columns = self.columns.len();
 
-        let raw_names: Vec<Vec<u8>> = self
+        let raw_refs: Vec<&[u8]> = self
             .columns
             .iter()
-            .map(|c| c.name.as_bytes().to_vec())
+            .map(|column| column.name.as_bytes())
             .collect();
-        let raw_refs: Vec<&[u8]> = raw_names.iter().map(|v| 
v.as_slice()).collect();
 
         let plain_size = front_coded_size(&raw_refs);
 
@@ -254,12 +253,8 @@ impl MosaicSchema {
         let mut bpe_size = usize::MAX;
 
         if bpe::is_ascii_only(&raw_refs) {
-            let rules = bpe::build_vocabulary(&raw_refs);
+            let (rules, names) = bpe::build_vocabulary_and_encode(&raw_refs);
             if !rules.is_empty() {
-                let names: Vec<Vec<u8>> = raw_refs
-                    .iter()
-                    .map(|name| bpe::encode(name, &rules))
-                    .collect();
                 let name_refs: Vec<&[u8]> = names.iter().map(|v| 
v.as_slice()).collect();
                 bpe_size = 1 + rules.len() * 2 + front_coded_size(&name_refs);
                 bpe_rules = rules;

Reply via email to