66#include < algorithm> // for copy
77#include < cstddef> // for size_t
88#include < memory> // for make_unique
9+ #include < mutex> // for lock_guard, scoped_lock
910#include < utility> // for move
1011#include < vector> // for vector
1112
1617#include " xgboost/json.h" // for Json
1718
1819namespace xgboost {
20+ namespace {
21+ // Validate Arrow StringArray offset invariants before the copy; malformed offsets cause
22+ // SortNames to compute OOB substrings and break stable_sort's strict-weak-ordering.
23+ void ValidateCatStrArrayOffsets (enc::CatStrArrayView const & str) {
24+ if (str.offsets .empty ()) {
25+ return ;
26+ }
27+ constexpr auto kHint =
28+ " The producing dataframe library is emitting inconsistent Arrow data; update it"
29+ " to the latest version." ;
30+ CHECK_EQ (str.offsets .front (), 0 )
31+ << " Malformed Arrow categorical dictionary: offsets[0] must be 0." << kHint ;
32+ auto const n = str.offsets .size ();
33+ for (std::size_t i = 0 ; i < n; ++i) {
34+ auto const off = str.offsets [i];
35+ CHECK_GE (off, 0 )
36+ << " Malformed Arrow categorical dictionary: offsets[" << i << " ] = " << off
37+ << " is negative." << kHint ;
38+ if (i + 1 < n) {
39+ CHECK_LE (off, str.offsets [i + 1 ])
40+ << " Malformed Arrow categorical dictionary: offsets not monotonic at i=" << i
41+ << " ." << kHint ;
42+ }
43+ }
44+ auto last = static_cast <std::size_t >(str.offsets .back ());
45+ CHECK_LE (last, str.values .size ())
46+ << " Malformed Arrow categorical dictionary: last offset " << last
47+ << " exceeds values buffer size " << str.values .size () << " ." << kHint ;
48+ }
49+ } // namespace
50+
1951CatContainer::CatContainer (enc::HostColumnsView const & df, bool is_ref) : CatContainer{} {
2052 this ->is_ref_ = is_ref;
2153 this ->n_total_cats_ = df.n_total_cats ;
@@ -30,6 +62,7 @@ CatContainer::CatContainer(enc::HostColumnsView const& df, bool is_ref) : CatCon
3062 for (auto const & col : df.columns ) {
3163 std::visit (enc::Overloaded{
3264 [this ](enc::CatStrArrayView str) {
65+ ValidateCatStrArrayOffsets (str);
3366 using T = typename cpu_impl::ViewToStorageImpl<enc::CatStrArrayView>::Type;
3467 this ->cpu_impl_ ->columns .emplace_back ();
3568 this ->cpu_impl_ ->columns .back ().emplace <T>();
@@ -116,6 +149,8 @@ struct PrimToUbj<double> {
116149} // anonymous namespace
117150
118151void CatContainer::Save (Json* p_out) const {
152+ // serializes the full container snapshot against Sort()/Copy()
153+ std::lock_guard guard{sort_mu_};
119154 [[maybe_unused]] auto _ = this ->HostView ();
120155 auto & out = *p_out;
121156
@@ -166,6 +201,9 @@ void CatContainer::Save(Json* p_out) const {
166201 out[" sorted_idx" ] = std::move (jsorted_index);
167202 out[" feature_segments" ] = std::move (jf_segments);
168203 out[" enc" ] = arr;
204+ // persist is_ref_ and sorted_; optional fields for back-compat with pre-field models
205+ out[" is_ref" ] = Boolean{this ->is_ref_ };
206+ out[" sorted" ] = Boolean{this ->sorted_ };
169207}
170208
171209namespace {
@@ -187,6 +225,8 @@ void LoadJson(Json jvalues, Vec* p_out) {
187225} // namespace
188226
189227void CatContainer::Load (Json const & in) {
228+ // serializes the full container snapshot against Sort()/Copy()
229+ std::lock_guard guard{sort_mu_};
190230 auto array = get<Array const >(in[" enc" ]);
191231 auto n_features = array.size ();
192232
@@ -266,6 +306,19 @@ void CatContainer::Load(Json const& in) {
266306 auto & h_sorted_idx = this ->sorted_idx_ .HostVector ();
267307 LoadJson<std::int32_t >(in[" sorted_idx" ], &h_sorted_idx);
268308
309+ // back-compat: missing fields default to is_ref=false, sorted=!sorted_idx.empty()
310+ auto const & obj = get<Object const >(in);
311+ if (auto it = obj.find (" is_ref" ); it != obj.cend ()) {
312+ this ->is_ref_ = get<Boolean const >(it->second );
313+ } else {
314+ this ->is_ref_ = false ;
315+ }
316+ if (auto it = obj.find (" sorted" ); it != obj.cend ()) {
317+ this ->sorted_ = get<Boolean const >(it->second );
318+ } else {
319+ this ->sorted_ = !h_sorted_idx.empty ();
320+ }
321+
269322 this ->cpu_impl_ ->Finalize ();
270323}
271324
@@ -275,6 +328,12 @@ CatContainer::CatContainer() : cpu_impl_{std::make_unique<cpu_impl::CatContainer
275328CatContainer::~CatContainer () = default ;
276329
277330void CatContainer::Copy (Context const * ctx, CatContainer const & that) {
331+ if (&that == this ) {
332+ return ;
333+ }
334+ // scoped_lock serializes concurrent a.Copy(b)+b.Copy(a); this->device_mu_ guards
335+ // destination writes against a concurrent this->HostView() on another thread
336+ std::scoped_lock guard{this ->sort_mu_ , that.sort_mu_ , this ->device_mu_ };
278337 [[maybe_unused]] auto h_view = that.HostView ();
279338 this ->CopyCommon (ctx, that);
280339 this ->cpu_impl_ ->Copy (that.cpu_impl_ .get ());
@@ -290,9 +349,16 @@ void CatContainer::Copy(Context const* ctx, CatContainer const& that) {
290349
291350void CatContainer::Sort (Context const * ctx) {
292351 CHECK (ctx->IsCPU ());
352+ // sort_mu_ serializes Sort()/Copy(); HasCategorical() reads n_total_cats_ which
353+ // Copy() writes under sort_mu_, so check inside the lock
354+ std::lock_guard guard{sort_mu_};
355+ if (!this ->HasCategorical () || this ->sorted_ ) {
356+ return ;
357+ }
293358 auto view = this ->HostView ();
294359 this ->sorted_idx_ .HostVector ().resize (view.n_total_cats );
295360 enc::SortNames (enc::Policy<EncErrorPolicy>{}, view, this ->sorted_idx_ .HostSpan ());
361+ this ->sorted_ = true ;
296362}
297363#endif // !defined(XGBOOST_USE_CUDA)
298364
0 commit comments