Multi-Round Lazy Start Merge
Background
Efficiently merging sorted data partitions at scale is crucial for a variety of training data preparation workloads, especially for Generative Recommenders (GRs) a new paradigm introduced in the paper Actions Speak Louder than Words: Trillion-Parameter Sequential Transducers for Generative Recommendations. A key requirement is to merge training data across partitions—for example, merging hourly partitions into daily ones—while ensuring that all rows sharing the same primary key are stored consecutively. Training data is typically partitioned and bucketed by primary key, with rows sharing the same key stored consecutively, so merging across partitions essentially becomes a multi-way merge problem.
Normally, Apache Spark can be used for this sort-merge requirement — for example, via CLUSTER BY.
However, training datasets for a single job can often reach the PB scale, which in turn generates shuffle data at PB scale.
Although we typically apply bucketing and ordering by key when preparing training data in production,
Spark can eliminate the shuffle when merging training data from multiple hourly partitions.
However, each Spark task can only read the files planned from various partitions within a split
sequentially, placing them into the sorter and spilling as needed. Only after all files have been read
does Spark perform a sort-merge of the spilled files. This process produces a large number of small
spill files, which further degrades efficiency.
Moreover, Spark’s spill is row-based with a low compression ratio, resulting in approximately 4 times
amplification compared to the original columnar training data in the data lake. These factors
significantly degrade task stability and performance. Velox has a LocalMerge operator that can be
introduced into Apache Spark via Gluten or PySpark on Velox.
Note: To keep the focus on merging, the remainder of this article also assumes that each partition’s training data is already sorted by primary key—a common setup in training data pipelines.
LocalMerge Operator
The LocalMerge operator consolidates its sources’ outputs into a single, sorted stream of rows.
It runs single-threaded, while its upstream sources may run multi-threaded within the same task,
producing multiple sorted inputs concurrently. For example, when merging 24 hourly partitions into
a single daily partition (as shown in the figure below), the merge plan fragment is split into two pipelines:
- Pipeline 0: contains two operators,
TableScanandCallbackSink. 24 drivers are instantiated to scan the 24 hourly partitions. - Pipeline 1: contains only a single operator,
LocalMerge, with one driver responsible for performing the sort merge.
A CallbackSink operator is installed at the end of each driver in Pipeline 0. It pushes the TableScan
operator’s output vectors into the queues backing the merge streams. Inside LocalMerge, a TreeOfLosers
performs a k-way merge over the 24 merge streams supplied by the Pipeline 0 drivers, producing a single,
globally sorted output stream.



