Repository navigation
[memory] layout: fill a categorical feature only when its code changes, in one copy when dense - #66
Conversation
2a2bfda to
f34c9d0
Compare
f34c9d0 to
137a19a
Compare
ukaratay
left a comment
There was a problem hiding this comment.
Approving. The skip is safe because fill_with is the only writer of a predictor's bins (kernel.rs:179), and the build asserts that the no-category code has no exceptions.
Two asks:
- How much of the −18.8% is the skip, and how much the dense copy? If the copy is within noise, drop it and
RESTORE_DENSITYwith it. - Where does the generated requests' skew come from? If we have logged requests, replay them or set the skew from them. As it stands the headline number depends on a generator parameter.
… copy when dense A predictor's bins keep each categorical feature's last code, and fill_with restored that code's exceptions to the defaults and wrote the new code's for every row or group, even when the code was the same. For a large model with many categorical splits, a popular category can have many exceptions, so requests that repeat popular categories spent much of their time there. fill_with now leaves a feature whose code is unchanged as it is, and when the held code's exceptions exceed an eighth of the feature's slots, restores the feature's slots in one copy from the defaults rather than one by one. A predictor's bins start as the defaults holding the no-category code, which has no exceptions, so an unchanged code's slots already hold its words. The outputs are unchanged.
137a19a to
d90ca72
Compare
|
Both measured on the private model; the description has the tables.
|
Motivation
A predictor's bins keep each categorical feature's last code, and
fill_withrestored that code's exceptions to the defaults and wrote the new code's for every row or group, even when the code was the same. For a model with many categorical splits a popular category has thousands of exceptions, so fills take much of a request's time: restoring the held code's exceptions one by one, even to write the same code again. The stack's first PR measured this on a large private model, with generated requests whose categories are skewed toward the most frequent.Design
layout: fill a categorical feature only when its code changes, in one copy when densefill_withis their only writer, so an unchanged code's slots already hold its words.RESTORE_DENSITY), the feature's slots are restored by one copy from the defaults rather than one by one; a feature's exceptions all lie in its own slots.Evidence
predict_fixedper request on the generated requests, one thread, Apple M4 Pro. p50 change from the PR below, median of four paired rounds (A B B A, 1 s per cell), with the rounds' range; every round is faster:Against 3.2.2 at 1 candidate (three interleaved rounds, within ±3%): #65 101%, this PR 82%.
The skip's gain depends on how often consecutive groups repeat a feature's category, and the requests are generated, so the two parts are split on four patterns of the same requests' categorical codes: as generated (a feature's category repeats between consecutive candidates 11.2% of the time), uniform over the model's categories (7.3%), never repeating, and always repeating. On AVX-512 (GCP c4-standard-8, Intel Xeon Platinum 8581C, SMT off, all-core turbo, one pinned core; six paired rounds), p50 change:
On the M4 at 1 candidate (four rounds, agreeing in sign but for the last cell), in the same order: skip only −8.7%, −8.5%, −7.7% and −38.7%; the copy on top −10.5%, −10.6%, −11.2% and within noise. At 32 and 512 candidates the M4's rounds were too noisy to split.
So the copy carries the gain wherever codes change. The skip pays when categories repeat, by about a third when every one does; at the generated and uniform rates it measures close to when none repeats, so most of the skip's small gain there (2% on AVX-512, 8% on the M4) is code layout, and the skip's own gain scales with how often categories repeat.
cargo test --release --features researchpasses; clippy (workspace, research, library alone) and fmt.