Following up on #12306 and #12307 (single-row CPU inplace-predict overhead): after both fixes, I re-profiled the same workload (single row, 127 features, 50 trees, nthread=1, XGBoosterPredictFromDense in a loop via a proxy DMatrix, Apple M3 Pro, ~4.5us/call) to see what remains. The call is still dominated by per-call setup rather than tree traversal:
| Cost |
~share of call |
Notes |
Json::Load of the config string |
~17% |
config bytes are typically identical call-to-call |
Json::Load of the array-interface string (inside ArrayAdapter) |
~16% |
only the data pointer/shape change between calls |
HostModel / GBTreeModelView construction |
~19% |
rebuilt per call: per-tree view variants, allocations, memcpys — model unchanged between calls |
Actual prediction work (FVecFill + traversal) |
~15% |
|
SetArray remainder, output handling, misc |
rest |
|
So roughly half of a single-row inplace predict is recomputing state that is invariant across calls in a serving loop. Three directions, in decreasing value/effort ratio:
- Cache the constructed model view (
HostModel) in PredictionCacheEntry (or on GBTree), invalidated when trees are added or tree_begin/tree_end differ. This also benefits the DMatrix predict path.
- Memoize the parsed prediction config: if the config bytes equal the previous call's (per booster, thread-local), reuse the parsed
Json. Alternatively a "prepared predict" handle in the C API, though that's a bigger surface change.
- Lighter array-interface parsing for the adapter path (the document schema is small and fixed; a specialized parser or a
Json::Load fast path could cut it substantially).
Before writing any code: is there maintainer appetite for work in this direction, and if so, which approach would you prefer for (1) and (2)? Happy to implement and benchmark — same methodology as the two merged PRs.
🤖 Generated with Claude Code
Following up on #12306 and #12307 (single-row CPU inplace-predict overhead): after both fixes, I re-profiled the same workload (single row, 127 features, 50 trees,
nthread=1,XGBoosterPredictFromDensein a loop via a proxy DMatrix, Apple M3 Pro, ~4.5us/call) to see what remains. The call is still dominated by per-call setup rather than tree traversal:Json::Loadof the config stringJson::Loadof the array-interface string (insideArrayAdapter)HostModel/GBTreeModelViewconstructionFVecFill+ traversal)SetArrayremainder, output handling, miscSo roughly half of a single-row inplace predict is recomputing state that is invariant across calls in a serving loop. Three directions, in decreasing value/effort ratio:
HostModel) inPredictionCacheEntry(or onGBTree), invalidated when trees are added ortree_begin/tree_enddiffer. This also benefits the DMatrix predict path.Json. Alternatively a "prepared predict" handle in the C API, though that's a bigger surface change.Json::Loadfast path could cut it substantially).Before writing any code: is there maintainer appetite for work in this direction, and if so, which approach would you prefer for (1) and (2)? Happy to implement and benchmark — same methodology as the two merged PRs.
🤖 Generated with Claude Code