True split-dict: partition dictionary across both halves

Importance-based partitioning — highest-importance entries on left
(central) for zero-latency local lookup, remainder on right
(peripheral) queried over BLE on miss. Both halves embed their own
MPHF binary. Configurable block size for tighter compression.
This commit is contained in:
afiqzudinhadi 2026-07-02 11:08:28 +08:00
parent 9d2d5e4e2d
commit 20218fa2ab
7 changed files with 209 additions and 58 deletions

View file

@ -9,30 +9,31 @@ target_include_directories(app PRIVATE
) )
# ── Split-dict mode ────────────────────────────────── # ── Split-dict mode ──────────────────────────────────
# Central: behavior engine + BLE dict client # TRUE split: both halves have local MPHF partition + BLE fallback
# Peripheral: dict embed + lookup engine + GATT server # Central: behavior engine + local left partition + BLE client
# Peripheral: local right partition + GATT server
if(CONFIG_STENO_SPLIT_DICT) if(CONFIG_STENO_SPLIT_DICT)
if(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) if(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
# Central side: behavior engine queries peripheral over BLE # Central: behavior engine + local dict + BLE client for remote partition
target_sources(app PRIVATE target_sources(app PRIVATE
src/behavior_steno.c src/behavior_steno.c
src/output.c src/output.c
src/formatter.c src/formatter.c
src/undo.c src/undo.c
src/dict_embed.S
src/dict_mphf.c
src/split_dict.c src/split_dict.c
src/split_cache.c src/split_cache.c
) )
else() else()
# Peripheral side: dict embedded here, serves GATT queries # Peripheral: local dict + GATT server for central queries
target_sources(app PRIVATE target_sources(app PRIVATE
src/dict_embed.S src/dict_embed.S
src/dict_mphf.c
src/split_dict.c
src/split_cache.c
) )
if(CONFIG_STENO_DICT_MPHF)
target_sources(app PRIVATE src/dict_mphf.c)
else()
target_sources(app PRIVATE src/trie.c)
endif()
endif() endif()
# ── Non-split mode ─────────────────────────────────── # ── Non-split mode ───────────────────────────────────
@ -55,12 +56,11 @@ else()
endif() endif()
# ── Dictionary compilation ─────────────────────────── # ── Dictionary compilation ───────────────────────────
# Build dict binary when we embed it (non-split, or split peripheral) # Both halves need dict embed in split mode; non-split same as before
set(STENO_NEED_DICT_EMBED FALSE) set(STENO_NEED_DICT_EMBED FALSE)
if(CONFIG_STENO_SPLIT_DICT) if(CONFIG_STENO_SPLIT_DICT)
if(NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL) # Both central and peripheral need their own partition
set(STENO_NEED_DICT_EMBED TRUE) set(STENO_NEED_DICT_EMBED TRUE)
endif()
elseif(NOT CONFIG_ZMK_SPLIT OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) elseif(NOT CONFIG_ZMK_SPLIT OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
set(STENO_NEED_DICT_EMBED TRUE) set(STENO_NEED_DICT_EMBED TRUE)
endif() endif()
@ -96,19 +96,43 @@ if(STENO_NEED_DICT_EMBED)
# Compile dict binary # Compile dict binary
if(EXISTS ${STENO_DICT_SRC}) if(EXISTS ${STENO_DICT_SRC})
if(CONFIG_STENO_DICT_MPHF) # Fetch at build time if hash changed
# Fetch at build time if hash changed if(DEFINED STENO_DICT_NAME)
if(DEFINED STENO_DICT_NAME) add_custom_command(
add_custom_command( OUTPUT ${STENO_DICT_SRC}.stamp
OUTPUT ${STENO_DICT_SRC}.stamp COMMAND ${Python3_EXECUTABLE} ${STENO_FETCH}
COMMAND ${Python3_EXECUTABLE} ${STENO_FETCH} ${STENO_DICT_NAME} ${STENO_DICTS_DIR}
${STENO_DICT_NAME} ${STENO_DICTS_DIR} COMMAND ${CMAKE_COMMAND} -E touch ${STENO_DICT_SRC}.stamp
COMMAND ${CMAKE_COMMAND} -E touch ${STENO_DICT_SRC}.stamp COMMENT "Checking ${STENO_DICT_NAME} dictionary for updates"
COMMENT "Checking ${STENO_DICT_NAME} dictionary for updates" )
) add_custom_target(steno_dict_fetch DEPENDS ${STENO_DICT_SRC}.stamp)
add_custom_target(steno_dict_fetch DEPENDS ${STENO_DICT_SRC}.stamp) endif()
if(CONFIG_STENO_SPLIT_DICT)
# Split mode: compile the appropriate partition
if(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
set(STENO_SPLIT_PART "left")
set(STENO_SPLIT_SIZE ${CONFIG_STENO_DICT_LEFT_MAX_SIZE})
else()
set(STENO_SPLIT_PART "right")
set(STENO_SPLIT_SIZE ${CONFIG_STENO_DICT_RIGHT_MAX_SIZE})
endif() endif()
add_custom_command(
OUTPUT ${STENO_DICT_BIN}
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/compile_mphf.py
${STENO_DICT_SRC} ${STENO_DICT_BIN}
--split-part ${STENO_SPLIT_PART}
--left-size ${CONFIG_STENO_DICT_LEFT_MAX_SIZE}
--right-size ${CONFIG_STENO_DICT_RIGHT_MAX_SIZE}
--max-size ${STENO_SPLIT_SIZE}
--block-size 2048
DEPENDS ${STENO_DICT_SRC}
${CMAKE_CURRENT_SOURCE_DIR}/tools/compile_mphf.py
COMMENT "Compiling steno dictionary (${STENO_SPLIT_PART} partition)"
)
elseif(CONFIG_STENO_DICT_MPHF)
add_custom_command( add_custom_command(
OUTPUT ${STENO_DICT_BIN} OUTPUT ${STENO_DICT_BIN}
COMMAND ${Python3_EXECUTABLE} COMMAND ${Python3_EXECUTABLE}

28
Kconfig
View file

@ -81,15 +81,31 @@ config STENO_DICT_MAX_SIZE
The MPHF compiler auto-trims to fit. The MPHF compiler auto-trims to fit.
menuconfig STENO_SPLIT_DICT menuconfig STENO_SPLIT_DICT
bool "Split dictionary storage on peripheral" bool "Split dictionary across both halves"
default n default n
select STENO_DICT_MPHF
help help
Store the steno dictionary on the peripheral half and Partition the steno dictionary across both keyboard halves.
perform lookups over BLE. This frees flash on the central Left (central) gets highest-importance entries for zero-latency
side at the cost of added lookup latency. local lookup. Right (peripheral) gets remaining entries, queried
over BLE on local miss. Both halves have their own MPHF dict.
if STENO_SPLIT_DICT if STENO_SPLIT_DICT
config STENO_DICT_LEFT_MAX_SIZE
int "Left half dict budget (bytes)"
default 430080
help
Flash budget for left (central) partition. Default 420KB.
Highest-importance entries fill this first.
config STENO_DICT_RIGHT_MAX_SIZE
int "Right half dict budget (bytes)"
default 545792
help
Flash budget for right (peripheral) partition. Default 533KB.
Remaining entries after left partition is filled.
config STENO_SPLIT_CACHE_SIZE config STENO_SPLIT_CACHE_SIZE
int "LRU cache entries on central side" int "LRU cache entries on central side"
default 64 default 64
@ -104,6 +120,10 @@ config STENO_SPLIT_TIMEOUT_MS
default 50 default 50
range 10 500 range 10 500
config STENO_SPLIT_LOG_LEVEL
int "Split dict log level"
default 3
endif # STENO_SPLIT_DICT endif # STENO_SPLIT_DICT
endif # STENO_ENGINE endif # STENO_ENGINE

View file

@ -18,6 +18,7 @@
#if IS_ENABLED(CONFIG_STENO_SPLIT_DICT) #if IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
#include "split_dict.h" #include "split_dict.h"
#include "dict_mphf.h"
#elif IS_ENABLED(CONFIG_STENO_DICT_MPHF) #elif IS_ENABLED(CONFIG_STENO_DICT_MPHF)
#include "dict_mphf.h" #include "dict_mphf.h"
#else #else
@ -26,12 +27,10 @@
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#if !IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
extern const uint8_t _steno_dict_start[]; extern const uint8_t _steno_dict_start[];
extern const uint8_t _steno_dict_end[]; extern const uint8_t _steno_dict_end[];
#endif
#if IS_ENABLED(CONFIG_STENO_DICT_MPHF) && !IS_ENABLED(CONFIG_STENO_SPLIT_DICT) #if IS_ENABLED(CONFIG_STENO_DICT_MPHF) || IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
static struct dict_mphf mphf_dict; static struct dict_mphf mphf_dict;
#endif #endif
@ -57,6 +56,12 @@ static void multi_timeout_handler(struct k_work *work);
static const char *do_lookup(const uint32_t *strokes, uint8_t count) static const char *do_lookup(const uint32_t *strokes, uint8_t count)
{ {
#if IS_ENABLED(CONFIG_STENO_SPLIT_DICT) #if IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
/* Try local partition first (zero latency) */
const char *local = dict_mphf_lookup(&mphf_dict, strokes, count);
if (local) {
return local;
}
/* Miss → query remote partition over BLE */
static char split_buf[128]; static char split_buf[128];
int ret = split_dict_lookup(strokes, count, split_buf, sizeof(split_buf)); int ret = split_dict_lookup(strokes, count, split_buf, sizeof(split_buf));
return (ret > 0) ? split_buf : NULL; return (ret > 0) ? split_buf : NULL;
@ -70,6 +75,11 @@ static const char *do_lookup(const uint32_t *strokes, uint8_t count)
static bool do_has_prefix(const uint32_t *strokes, uint8_t count) static bool do_has_prefix(const uint32_t *strokes, uint8_t count)
{ {
#if IS_ENABLED(CONFIG_STENO_SPLIT_DICT) #if IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
/* Check local prefix table first */
if (count == 1 && dict_mphf_has_prefix(&mphf_dict, strokes[0])) {
return true;
}
/* Fall through to remote */
return split_dict_has_prefix(strokes, count); return split_dict_has_prefix(strokes, count);
#elif IS_ENABLED(CONFIG_STENO_DICT_MPHF) #elif IS_ENABLED(CONFIG_STENO_DICT_MPHF)
return (count == 1) ? dict_mphf_has_prefix(&mphf_dict, strokes[0]) : false; return (count == 1) ? dict_mphf_has_prefix(&mphf_dict, strokes[0]) : false;
@ -225,6 +235,21 @@ static int behavior_steno_init(const struct device *dev)
k_work_init_delayable(&state.multi_timeout, multi_timeout_handler); k_work_init_delayable(&state.multi_timeout, multi_timeout_handler);
#if IS_ENABLED(CONFIG_STENO_SPLIT_DICT) #if IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
/* Init local partition dict */
{
size_t dict_size = _steno_dict_end - _steno_dict_start;
if (dict_size > 4) {
int ret = dict_mphf_init(&mphf_dict, _steno_dict_start, dict_size);
if (ret == 0) {
LOG_INF("Local partition loaded (%u bytes)", (unsigned)dict_size);
} else {
LOG_ERR("Local partition init failed: %d", ret);
}
} else {
LOG_WRN("No local partition embedded");
}
}
/* Init BLE client for remote partition */
split_dict_init(); split_dict_init();
dict_ready = true; dict_ready = true;
#else #else

View file

@ -188,6 +188,9 @@ int dict_mphf_init(struct dict_mphf *dict, const void *data, size_t len)
dict->blocks_start = dict->str_data_start; dict->blocks_start = dict->str_data_start;
} }
/* Block size from header (0 = legacy default 4096) */
dict->blk_size = hdr->block_size ? hdr->block_size : DICT_MPHF_BLOCK_SIZE;
/* Prefix table at end */ /* Prefix table at end */
uint32_t prefix_bytes = (uint32_t)hdr->prefix_count * 4; uint32_t prefix_bytes = (uint32_t)hdr->prefix_count * 4;
if (len >= prefix_bytes) { if (len >= prefix_bytes) {
@ -213,8 +216,8 @@ static const char *resolve_string(const struct dict_mphf *dict, uint32_t val_id)
} }
/* Block-compressed: decompress the right block */ /* Block-compressed: decompress the right block */
uint32_t block_idx = str_offset / DICT_MPHF_BLOCK_SIZE; uint32_t block_idx = str_offset / dict->blk_size;
uint32_t in_block_off = str_offset % DICT_MPHF_BLOCK_SIZE; uint32_t in_block_off = str_offset % dict->blk_size;
if (block_idx >= dict->block_count) { if (block_idx >= dict->block_count) {
return NULL; return NULL;

View file

@ -31,7 +31,7 @@ struct dict_mphf_header {
uint8_t value_bits; uint8_t value_bits;
uint8_t disp_bits; uint8_t disp_bits;
uint16_t prefix_count; uint16_t prefix_count;
uint32_t reserved0; uint32_t block_size; /* zlib block size (0 = default 4096) */
uint32_t reserved1; uint32_t reserved1;
} __attribute__((packed)); } __attribute__((packed));
@ -52,6 +52,7 @@ struct dict_mphf {
const uint32_t *block_dir; /* block offset directory */ const uint32_t *block_dir; /* block offset directory */
const uint8_t *blocks_start; /* start of compressed blocks */ const uint8_t *blocks_start; /* start of compressed blocks */
uint32_t str_data_len; /* total string data section length */ uint32_t str_data_len; /* total string data section length */
uint32_t blk_size; /* actual block size from header */
}; };
int dict_mphf_init(struct dict_mphf *dict, const void *data, size_t len); int dict_mphf_init(struct dict_mphf *dict, const void *data, size_t len);

View file

@ -18,6 +18,7 @@
#include "split_dict.h" #include "split_dict.h"
#include "split_cache.h" #include "split_cache.h"
#include "dict_mphf.h"
LOG_MODULE_REGISTER(split_dict, CONFIG_STENO_SPLIT_LOG_LEVEL); LOG_MODULE_REGISTER(split_dict, CONFIG_STENO_SPLIT_LOG_LEVEL);
@ -33,10 +34,11 @@ static uint8_t seq_counter;
/* Cache instance */ /* Cache instance */
static struct split_cache dict_cache; static struct split_cache dict_cache;
/* External trie lookup (peripheral side) */ /* Local MPHF dict for peripheral-side GATT lookups */
extern int trie_lookup(const uint32_t *strokes, uint8_t count, extern const uint8_t _steno_dict_start[];
char *result, size_t result_size); extern const uint8_t _steno_dict_end[];
extern bool trie_has_prefix(const uint32_t *strokes, uint8_t count); static struct dict_mphf peripheral_mphf;
static bool peripheral_dict_ready;
/* --- Helpers --- */ /* --- Helpers --- */
@ -91,14 +93,17 @@ static ssize_t dict_query_write_cb(struct bt_conn *conn,
resp->msg_type = STENO_MSG_RESPONSE; resp->msg_type = STENO_MSG_RESPONSE;
resp->seq = pkt->seq; resp->seq = pkt->seq;
char translation[128]; const char *translation = NULL;
int ret = trie_lookup(strokes, stroke_count, translation, sizeof(translation)); if (peripheral_dict_ready) {
translation = dict_mphf_lookup(&peripheral_mphf, strokes, stroke_count);
}
if (ret > 0) { if (translation) {
uint16_t tlen = (uint16_t)strlen(translation);
resp->status = STENO_STATUS_FOUND; resp->status = STENO_STATUS_FOUND;
resp->data_len = (uint16_t)ret; resp->data_len = tlen;
memcpy(resp->data, translation, ret); memcpy(resp->data, translation, tlen);
response_len = sizeof(struct steno_response_pkt) + ret; response_len = sizeof(struct steno_response_pkt) + tlen;
} else { } else {
resp->status = STENO_STATUS_NOT_FOUND; resp->status = STENO_STATUS_NOT_FOUND;
resp->data_len = 0; resp->data_len = 0;
@ -135,7 +140,9 @@ static ssize_t dict_prefix_write_cb(struct bt_conn *conn,
resp->seq = pkt->seq; resp->seq = pkt->seq;
resp->data_len = 0; resp->data_len = 0;
if (trie_has_prefix(strokes, stroke_count)) { if (peripheral_dict_ready &&
stroke_count == 1 &&
dict_mphf_has_prefix(&peripheral_mphf, strokes[0])) {
resp->status = STENO_STATUS_PREFIX_ONLY; resp->status = STENO_STATUS_PREFIX_ONLY;
} else { } else {
resp->status = STENO_STATUS_NOT_FOUND; resp->status = STENO_STATUS_NOT_FOUND;
@ -188,14 +195,17 @@ static ssize_t dict_batch_write_cb(struct bt_conn *conn,
resp->msg_type = STENO_MSG_RESPONSE; resp->msg_type = STENO_MSG_RESPONSE;
resp->seq = pkt->seq; resp->seq = pkt->seq;
char translation[128]; const char *translation = NULL;
int ret = trie_lookup(strokes, stroke_count, translation, sizeof(translation)); if (peripheral_dict_ready) {
translation = dict_mphf_lookup(&peripheral_mphf, strokes, stroke_count);
}
if (ret > 0) { if (translation) {
uint16_t tlen = (uint16_t)strlen(translation);
resp->status = STENO_STATUS_FOUND; resp->status = STENO_STATUS_FOUND;
resp->data_len = (uint16_t)ret; resp->data_len = tlen;
memcpy(resp->data, translation, ret); memcpy(resp->data, translation, tlen);
response_len = sizeof(struct steno_response_pkt) + ret; response_len = sizeof(struct steno_response_pkt) + tlen;
} else { } else {
resp->status = STENO_STATUS_NOT_FOUND; resp->status = STENO_STATUS_NOT_FOUND;
resp->data_len = 0; resp->data_len = 0;
@ -453,6 +463,18 @@ int split_dict_init(void)
seq_counter = 0; seq_counter = 0;
split_conn = NULL; split_conn = NULL;
/* Init peripheral-side MPHF dict for GATT lookups */
size_t dict_size = _steno_dict_end - _steno_dict_start;
if (dict_size > 4) {
int ret = dict_mphf_init(&peripheral_mphf, _steno_dict_start, dict_size);
if (ret == 0) {
peripheral_dict_ready = true;
LOG_INF("Peripheral partition loaded (%u bytes)", (unsigned)dict_size);
} else {
LOG_ERR("Peripheral partition init failed: %d", ret);
}
}
LOG_INF("Split dict initialized"); LOG_INF("Split dict initialized");
return 0; return 0;
} }

View file

@ -230,10 +230,11 @@ def build_chd(keys_and_bytes, entry_count):
# ─── Compilation ─── # ─── Compilation ───
def compile_mphf(entries, max_size=None): def compile_mphf(entries, max_size=None, block_size=4096):
""" """
entries: list of (stroke_str, translation) from JSON dict entries: list of (stroke_str, translation) from JSON dict
max_size: max output size in bytes (default: 462*1024 = 473088) max_size: max output size in bytes (default: 462*1024 = 473088)
block_size: zlib compression block size (smaller = better ratio, more blocks)
Returns: bytes (the compiled binary) or None if can't fit Returns: bytes (the compiled binary) or None if can't fit
""" """
@ -286,7 +287,6 @@ def compile_mphf(entries, max_size=None):
string_offsets.append(len(string_data_raw)) string_offsets.append(len(string_data_raw))
string_data_raw += t.encode('utf-8') + b'\x00' string_data_raw += t.encode('utf-8') + b'\x00'
block_size = 4096
compressed_blocks = [] compressed_blocks = []
for i in range(0, len(string_data_raw), block_size): for i in range(0, len(string_data_raw), block_size):
block = string_data_raw[i:i + block_size] block = string_data_raw[i:i + block_size]
@ -388,7 +388,7 @@ def compile_mphf(entries, max_size=None):
# magic: u32, version: u16, flags: u16, # magic: u32, version: u16, flags: u16,
# entry_count: u32, bucket_count: u32, unique_count: u32, # entry_count: u32, bucket_count: u32, unique_count: u32,
# value_bits: u8, disp_bits: u8, prefix_count: u16, # value_bits: u8, disp_bits: u8, prefix_count: u16,
# reserved0: u32, reserved1: u32 # block_size: u32, reserved1: u32
header = struct.pack('<IHHIIIBBHii', header = struct.pack('<IHHIIIBBHii',
0x4F4E5453, # magic "STNO" 0x4F4E5453, # magic "STNO"
2, # version 2, # version
@ -399,7 +399,7 @@ def compile_mphf(entries, max_size=None):
value_bits, # value_bits value_bits, # value_bits
disp_bits, # disp_bits disp_bits, # disp_bits
prefix_count, # prefix_count prefix_count, # prefix_count
0, # reserved0 block_size, # block_size (was reserved0)
0, # reserved1 0, # reserved1
) )
assert len(header) == 32, f"Header is {len(header)} bytes, expected 32" assert len(header) == 32, f"Header is {len(header)} bytes, expected 32"
@ -475,6 +475,7 @@ def compile_mphf(entries, max_size=None):
'disp_bits': disp_bits, 'disp_bits': disp_bits,
'max_displacement': max_disp, 'max_displacement': max_disp,
'prefix_count': prefix_count, 'prefix_count': prefix_count,
'block_size': block_size,
'disp_section_bytes': len(disp_section), 'disp_section_bytes': len(disp_section),
'val_section_bytes': len(val_section), 'val_section_bytes': len(val_section),
'fp_section_bytes': len(fp_section), 'fp_section_bytes': len(fp_section),
@ -488,8 +489,8 @@ def compile_mphf(entries, max_size=None):
def print_stats(stats): def print_stats(stats):
"""Print size breakdown statistics.""" """Print size breakdown statistics."""
total = stats['entry_count']
print(f"Entries: {stats['entry_count']}") print(f"Entries: {stats['entry_count']}")
print(f"Block size: {stats.get('block_size', 4096)} bytes")
print(f"MPHF displacements: {stats['disp_section_bytes']/1024:.1f} KB " print(f"MPHF displacements: {stats['disp_section_bytes']/1024:.1f} KB "
f"({stats['bucket_count']} buckets, {stats['disp_bits']} bits each)") f"({stats['bucket_count']} buckets, {stats['disp_bits']} bits each)")
print(f"Value array: {stats['val_section_bytes']/1024:.1f} KB " print(f"Value array: {stats['val_section_bytes']/1024:.1f} KB "
@ -504,6 +505,32 @@ def print_stats(stats):
print(f"Total: {stats['total_bytes']/1024:.1f} KB") print(f"Total: {stats['total_bytes']/1024:.1f} KB")
def partition_entries(entries, left_budget, right_budget):
"""Partition dict entries by importance into left (central) and right (peripheral).
Left gets highest-importance entries first (most common single-stroke words).
Right gets remaining entries. Both are trimmed to their flash budgets via
compile_mphf's internal trimming.
Returns (left_entries, right_entries).
"""
sorted_entries = sorted(entries, key=lambda e: score_entry(e[0], e[1]))
# Rough estimate: ~15 bytes per entry average (conservative)
# compile_mphf will trim further if needed
est_bytes_per_entry = 15
left_max = left_budget // est_bytes_per_entry
right_max = right_budget // est_bytes_per_entry
# Cap to actual count
left_max = min(left_max, len(sorted_entries))
left_entries = sorted_entries[:left_max]
right_entries = sorted_entries[left_max:]
return left_entries, right_entries
def main(): def main():
parser = argparse.ArgumentParser(description='Compile steno dictionary to MPHF binary format') parser = argparse.ArgumentParser(description='Compile steno dictionary to MPHF binary format')
parser.add_argument('input', help='Input JSON dictionary (Plover format)') parser.add_argument('input', help='Input JSON dictionary (Plover format)')
@ -516,6 +543,14 @@ def main():
help='Print size breakdown statistics') help='Print size breakdown statistics')
parser.add_argument('--verify', action='store_true', default=True, parser.add_argument('--verify', action='store_true', default=True,
help='Verify compiled dict (default: true)') help='Verify compiled dict (default: true)')
parser.add_argument('--split-part', choices=['left', 'right'],
help='Build one partition of a split dict (left=central, right=peripheral)')
parser.add_argument('--left-size', type=int, default=430080,
help='Left partition flash budget in bytes (default: 430080 = 420KB)')
parser.add_argument('--right-size', type=int, default=545792,
help='Right partition flash budget in bytes (default: 545792 = 533KB)')
parser.add_argument('--block-size', type=int, default=4096,
help='Zlib compression block size (default: 4096, try 2048/1024 for tighter packing)')
args = parser.parse_args() args = parser.parse_args()
# Load dictionary # Load dictionary
@ -531,7 +566,26 @@ def main():
entries = entries_scored[:args.max_entries] entries = entries_scored[:args.max_entries]
print(f"Trimmed to {len(entries)} entries (--max-entries)", file=sys.stderr) print(f"Trimmed to {len(entries)} entries (--max-entries)", file=sys.stderr)
result = compile_mphf(entries, max_size=args.max_size) # Split-partition mode
if args.split_part:
left_entries, right_entries = partition_entries(
entries, args.left_size, args.right_size)
if args.split_part == 'left':
print(f"Split partition: LEFT (central) — {len(left_entries)} entries, "
f"budget {args.left_size} bytes", file=sys.stderr)
entries = left_entries
max_size = args.left_size
else:
print(f"Split partition: RIGHT (peripheral) — {len(right_entries)} entries, "
f"budget {args.right_size} bytes", file=sys.stderr)
entries = right_entries
max_size = args.right_size
else:
max_size = args.max_size
result = compile_mphf(entries, max_size=max_size,
block_size=args.block_size)
if result is None: if result is None:
print("Compilation failed", file=sys.stderr) print("Compilation failed", file=sys.stderr)
@ -544,8 +598,10 @@ def main():
print(f"Wrote {len(binary)} bytes to {args.output}", file=sys.stderr) print(f"Wrote {len(binary)} bytes to {args.output}", file=sys.stderr)
if args.stats: if args.stats or args.split_part:
print() print()
if args.split_part:
print(f"=== {args.split_part.upper()} partition ===")
print_stats(stats) print_stats(stats)