Skip to content

recipe: xgboost 3.4.1 - #125

Merged
ndonkoHenri merged 6 commits into
flet-dev:mainfrom
ndonkoHenri:xgboost
Oct 4, 2026
Merged

ndonkoHenri merged 6 commits into
flet-dev:mainfrom
ndonkoHenri:xgboost

Conversation

@ndonkoHenri

Copy link
Copy Markdown

Adds a recipe for xgboost 3.4.1 — gradient-boosted trees, the model family behind most tabular ML. Requested in flet#6912, to load .joblib models on a phone.

Upstream ships no Android or iOS wheels, and the PyPI wheels are host-only, so this is the only way to run a desktop-trained XGBoost model inside a Flet app.

Recipe shape

XGBoost 3.4 replaced its custom hatchling backend with scikit-build-core, so this is the soxr / duckdb shape: CMAKE_ARGS carries the cross toolchain, and the three backend hunks an earlier 3.3.0 draft of this recipe needed are gone. The Python package loads libxgboost through ctypes from a path built off __file__, and that is what the one mobile.patch is about:

  • iOS — serious_python moves the library into an embedded framework and leaves a .fwork pointer; libpath.py gets an ios branch that follows it (flet's iOS runtime reports ios on 3.12 too), and upstream's darwin branch stays untouched.
  • Android — Flet relocates the .so into the APK's native library directory, so libpath.py falls back to the bare soname when no on-disk candidate exists. SOVERSION is dropped so the soname matches the shipped file.
  • import xgboost crashed on Android under Flet 0.86+ with NotADirectoryError: …/sitepackages.zip/xgboost/VERSION: _c_api._py_version() opens the version file through __file__ on every import. The patch reads it through importlib.resources, which zipimport serves, so consumers need no extract_packages entry. A meta.yaml extract_packages would have made CI green while every consumer app still crashed — that copy only reaches the test app.

The same zip has a consumer-facing consequence the README leads its Install section with: import xgboost imports scikit-learn whenever it is installed, so an Android app that has scikit-learn for any reason must list it in extract_packages, or import xgboost fails.

Android builds with the NDK's OpenMP and links libomp.so (flet-libomp); iOS has no OpenMP runtime and builds with USE_OPENMP=OFF, which upstream supports. armeabi-v7a is excluded: 3.4.1 still fails at quantile.cc:39, narrowing into a 32-bit size_t.

Validation

  • 5/5 applicable slices built locally for cp314: arm64-v8a, x86_64, iphoneos.arm64, iphonesimulator.arm64, iphonesimulator.x86_64. Android DT_NEEDED is bionic + libc++_shared.so + libomp.so, soname libxgboost.so, every LOAD aligned 0x4000; iOS links only libc++/libSystem.
  • CI 6/6 jobs green across 3.12 / 3.13 / 3.14 × android / iOS, twice, dispatched with mobile_test_pythons=ALL: run 37148157624, then run 37167791098 on this exact tree after a claim audit. 5 passed / EXIT 0 on device in every leg of both.
  • The iris-explainer example (flet 1.0.3) runs on an Android arm64 emulator and the iOS Simulator: it loads a desktop-trained .joblib from assets without scikit-learn, and its predictions and TreeSHAP contributions match the desktop's to two decimals on both. A prediction plus explanation takes 0.5–3 ms on the emulator and 0.6 ms on the simulator.
  • A default flet build apk without target_arch fails with No matching distribution found for xgboost==3.4.1 on armeabi-v7a, as the README warns.

Changes

  • recipes/xgboost/ — meta.yaml, one patch, 5 on-device tests (incl. a 7.9 KB desktop-trained .joblib fixture loaded without scikit-learn, a booster thread cap, and the OpenMP flag per platform), README.md, and the example (pinned to flet 1.0.3).
  • .claude/skills/ — the zip-safe importlib.resources fix for __file__ data reads (and why meta-only extract_packages is a trap), the in-package ctypes-library recipe shape, and the local Xcode 27 iOS deployment-target workaround.

Consumer notes

A bare XGBClassifier/XGBRegressor saved with joblib loads and predicts on device without scikit-learn; a Pipeline needs scikit-learn as well. On Android XGBoost uses every core by default, on iOS one. import xgboost brings scipy, which is most of the size. Details, including model-version compatibility and the target_arch line, are in the recipe README.

Gradient boosting, the lightgbm sibling — same ctypes-C-API shape, very
different build system: a custom hatchling backend (packager/) that
drives CMake itself and reads no CMAKE_ARGS. The mobile.patch teaches it
three things: FORGE_CMAKE_ARGS injection (appended after its own args so
recipe toolchain/OpenMP flags win), FORGE_LIB_NAME (its _lib_name()
switches on the BUILD HOST platform, so a macOS-hosted cross build would
look for libxgboost.dylib while CMake produced .so), and — critically —
disabling its silent retry-WITHOUT-OpenMP on any cmake failure whenever
FORGE_CMAKE_ARGS is set (a broken cross setup would otherwise 'succeed'
as a degraded build). Plus the lightgbm ctypes-on-iOS treatment: lib
named .so on iOS with the VERSION/SOVERSION symlink chain dropped, and
libpath.py taught the 'ios' and 'android' sys.platform values (absent
upstream entirely — it would fail before ctypes even loaded) and the
.fwork pointer form.

iOS single-threaded (USE_OPENMP=OFF); Android real OpenMP (dynamic
libomp.so -> flet-libomp, same as lightgbm). armeabi-v7a excluded:
upstream assumes 64-bit size_t (narrowing hard error in quantile.cc)
and ships no 32-bit builds; lightgbm covers that arch. The sdist
variant has no nvidia-nccl dep (that marker lives in the cp-wheel
variants only).

5/5 applicable slices green; patched sdist proven on desktop CPython
3.14. Tests mirror lightgbm's: native xgb.train (no sklearn dep),
UBJSON save/load roundtrip, and pred_contribs TreeSHAP reconstructing
the margin.
3.4 replaced the custom hatchling backend with scikit-build-core, so the
three backend hunks the 3.3.0 recipe needed are gone and CMAKE_ARGS
carries the cross toolchain. What remains is running under Flet:

- iOS: name the library .so so serious_python framework-izes it, and let
  libpath.py find the .fwork pointer.
- Android: Flet moves the .so into the APK's native lib dir, so libpath.py
  falls back to the bare soname. _py_version() reads VERSION through
  importlib.resources: site-packages ships as a zip, and the old
  __file__ open() broke every `import xgboost` there.
- No SOVERSION on mobile, so the soname matches the shipped file.

Android links the NDK's libomp (flet-libomp); iOS builds without OpenMP.
armeabi-v7a stays excluded: quantile.cc still narrows into a 32-bit
size_t. Tests add a desktop-trained .joblib classifier, loaded without
scikit-learn, and the OpenMP flag per platform. README and the
iris-explainer example cover training on a desktop and predicting on
the device.

[skip ci]
- libpath.py: flet's iOS runtime reports "ios" on 3.12 too, so iOS gets
  its own branch and upstream's darwin branch stays byte-identical.
- README: with scikit-learn installed but not extracted, `import xgboost`
  itself fails on Android (xgboost imports sklearn eagerly and catches
  only ImportError). List what an unpickled model can and cannot do
  without scikit-learn, and that scikit-learn pickles need the version
  pypi.flet.dev carries. Thread count follows the pickled n_jobs. An APK
  carries the unpacked size.
- Test: the booster thread cap works on the desktop-trained model.
- Example: the load error keeps the exception type and path.

[skip ci]
- forge-error-catalogue: a meta-only extract_packages fix passes CI and
  breaks consumers; patch a package's own __file__ read to
  importlib.resources instead. xgboost as a ctypes-by-__file__ instance.
  flet's iOS 3.12 reports sys.platform "ios", not "darwin".
- new-mobile-recipe: the in-package ctypes C-API library shape.
- local-recipe-testing: finishing an iOS build by hand when a new Xcode
  rejects flet's 13.0 deployment target; flet 1.0.3 bundles 3.14.

[skip ci]
Flet calls a handler with no parameters without the event, so show()
needs no placeholder argument.
# Conflicts:
#	.claude/skills/local-recipe-testing/SKILL.md
@ndonkoHenri
ndonkoHenri merged commit 90778e4 into flet-dev:main Oct 4, 2026
6 of 12 checks passed
@ndonkoHenri
ndonkoHenri deleted the xgboost branch October 4, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant