msime-engine 0.2.0

Input method engine for Chinese and other languages: quanpin, shuangpin, nine-key, wubi, zhuyin, Cantonese, Japanese, Korean and more, ranked over a word lattice with typo correction and learning from what the user picks. The engine of 水杉输入法 (Metasequoia IME).
Documentation
use super::super::ngram::tests::pair_table;
use super::*;
use crate::ime::personal_rerank::allocations::measure;

#[path = "decode_row_index_legacy_tests.rs"]
mod legacy;

fn assert_storage(actual: &Vec<SentencePath>, expected: &Vec<SentencePath>) {
    super::capacity_tests::assert_paths(actual, expected);
    assert_eq!(actual.capacity(), expected.capacity());
    for (actual, expected) in actual.iter().zip(expected) {
        assert_eq!(actual.sentence.capacity(), expected.sentence.capacity());
        assert_eq!(actual.key.capacity(), expected.key.capacity());
        assert_eq!(actual.words.capacity(), expected.words.capacity());
        for (actual, expected) in actual.words.iter().zip(&expected.words) {
            assert_eq!(actual.capacity(), expected.capacity());
        }
    }
}

#[test]
fn small_bigram_row_index_skips_one_allocation() {
    let directory = tempfile::tempdir().unwrap();
    let table = Arc::new(pair_table(
        directory.path(),
        "pairs.bin",
        &[(SENTENCE_START, "甲", 1.0)],
    ));
    for columns in [1, 2, 8, 32] {
        let graph = super::capacity_tests::graph(columns, 1, 0, false);
        let options = LatticeOptions {
            bigram: Some(Arc::clone(&table)),
            ..LatticeOptions::default()
        };
        let (expected, before) = measure(|| legacy::decode_graph(&graph, &options, None));
        let (actual, after) = measure(|| decode_graph(&graph, &options, None));
        assert_storage(&actual, &expected);
        assert_eq!(after.remaining_bytes, before.remaining_bytes);
        assert_eq!(after.allocations + 1, before.allocations, "列数={columns}");
        assert!(after.peak_bytes < before.peak_bytes);
    }
    let graph = super::capacity_tests::graph(8, 4, 0, false);
    let options = LatticeOptions::default();
    let (expected, before) = measure(|| legacy::decode_graph(&graph, &options, None));
    let (actual, after) = measure(|| decode_graph(&graph, &options, None));
    assert_storage(&actual, &expected);
    assert_eq!(after.allocations, before.allocations);
    assert_eq!(after.remaining_bytes, before.remaining_bytes);
}

#[test]
fn small_bigram_row_index_preserves_wide_fallback_and_scores() {
    let directory = tempfile::tempdir().unwrap();
    let table = Arc::new(pair_table(
        directory.path(),
        "pairs.bin",
        &[
            (SENTENCE_START, "甲", 1.0),
            ("甲", "乙", -2.0),
            ("乙", "丙", 0.5),
        ],
    ));
    for columns in [0, 1, 4, 8, 9, 16, 32] {
        for width in [0, 1, 4, 8, 9, 16, 32, 65] {
            let mut graph = super::capacity_tests::graph(columns, width, 0, false);
            for (index, edge) in graph
                .iter_mut()
                .flat_map(|edges| edges.iter_mut())
                .enumerate()
            {
                edge.word = format!("词{index}");
                if index % 3 == 0 {
                    edge.log_prob = f64::NAN;
                }
            }
            let extra = super::capacity_tests::graph(columns, width, 1, true);
            for (beam, nbest) in [(0, 0), (1, 5), (4, 0), (8, 12), (32, 5), (65, 65)] {
                for with_extra in [false, true] {
                    let old_memo = RefCell::new(BigramMemo::default());
                    let new_memo = RefCell::new(BigramMemo::default());
                    let options = LatticeOptions {
                        beam,
                        nbest,
                        bigram: Some(Arc::clone(&table)),
                        bigram_memo: Some(&new_memo),
                        ..LatticeOptions::default()
                    };
                    let old_options = LatticeOptions {
                        bigram_memo: Some(&old_memo),
                        ..options.clone()
                    };
                    let extra = with_extra.then_some(&extra);
                    let expected = legacy::decode_graph(&graph, &old_options, extra);
                    let actual = decode_graph(&graph, &options, extra);
                    assert_storage(&actual, &expected);
                    assert_eq!(new_memo.borrow().scores, old_memo.borrow().scores);
                }
            }
        }
    }
}

#[test]
#[ignore = "同机 release 配对计时,普通 CI 不依赖共享机器速度"]
fn small_bigram_row_index_release_comparison() {
    use std::hint::black_box;
    use std::time::{Duration, Instant};

    let directory = tempfile::tempdir().unwrap();
    let table = Arc::new(pair_table(
        directory.path(),
        "pairs.bin",
        &[(SENTENCE_START, "甲", 1.0), ("甲", "乙", -2.0)],
    ));
    let shapes = [
        (0, 0, 32, 5),
        (1, 1, 32, 5),
        (4, 0, 32, 5),
        (4, 4, 32, 5),
        (4, 8, 32, 5),
        (12, 32, 32, 5),
        (32, 32, 32, 5),
        (4, 9, 32, 5),
        (12, 32, 32, 5),
        (4, 65, 65, 65),
        (4, 32, 4, 65),
        (12, 32, 32, 5),
    ];
    let mut regressions = Vec::new();
    let mut target_ratios = Vec::new();
    for &(columns, width, beam, nbest) in &shapes {
        let graph = super::capacity_tests::graph(columns, width, 0, false);
        let extra_graph = super::capacity_tests::graph(columns, width, 1, true);
        for mode in 0..3 {
            for with_extra in [false, true] {
                let extra = with_extra.then_some(&extra_graph);
                let repeats_for = |old: bool, repeats: usize| {
                    let start = Instant::now();
                    for _ in 0..repeats {
                        let memo = RefCell::new(BigramMemo::default());
                        let options = LatticeOptions {
                            beam,
                            nbest,
                            bigram: (mode > 0).then(|| Arc::clone(&table)),
                            bigram_memo: (mode == 2).then_some(&memo),
                            ..LatticeOptions::default()
                        };
                        let rows = if old {
                            legacy::decode_graph(
                                black_box(&graph),
                                black_box(&options),
                                black_box(extra),
                            )
                        } else {
                            decode_graph(black_box(&graph), black_box(&options), black_box(extra))
                        };
                        black_box(rows);
                    }
                    start.elapsed()
                };
                let mut repeats = 1;
                loop {
                    if repeats_for(true, repeats) >= Duration::from_millis(20) {
                        break;
                    }
                    repeats *= 2;
                    assert!(repeats < 1_000_000);
                }
                let mut ratios = Vec::new();
                for pair in 0..21 {
                    let elapsed = if pair % 2 == 0 {
                        [repeats_for(true, repeats), repeats_for(false, repeats)]
                    } else {
                        [repeats_for(false, repeats), repeats_for(true, repeats)]
                    };
                    let ratio = if pair % 2 == 0 {
                        elapsed[1].as_secs_f64() / elapsed[0].as_secs_f64()
                    } else {
                        elapsed[0].as_secs_f64() / elapsed[1].as_secs_f64()
                    };
                    ratios.push(ratio);
                }
                ratios.sort_by(f64::total_cmp);
                let median = ratios[10];
                eprintln!(
                    "small-row-index mode={mode} columns={columns} width={width} beam={beam} nbest={nbest} extra={with_extra} repeats={repeats} median={median:.6} p10={:.6} p90={:.6}",
                    ratios[2],
                    ratios[18]
                );
                if median >= 1.05 {
                    regressions.push((columns, width, beam, nbest, with_extra, median));
                }
                if mode > 0 && columns > 1 && width > 0 && !with_extra {
                    target_ratios.push(median);
                }
            }
        }
    }
    let target_geomean = (target_ratios.iter().map(|ratio| ratio.ln()).sum::<f64>()
        / target_ratios.len() as f64)
        .exp();
    eprintln!("small-row-index target geometric mean={target_geomean:.6}");
    assert!(target_geomean < 0.98, "小索引目标组没有达到收益门槛");
    assert!(regressions.is_empty(), "回退超过预算:{regressions:?}");
}