Skip to content

Port Vowpal Wabbit to the python bindings and upgrade to vowpalwabbit 9 - #2373

Open
miguelgfierro wants to merge 49 commits into
stagingfrom
miguelgfierro/vowpalwabbit-9
Open

Port Vowpal Wabbit to the python bindings and upgrade to vowpalwabbit 9#2373
miguelgfierro wants to merge 49 commits into
stagingfrom
miguelgfierro/vowpalwabbit-9

Conversation

@miguelgfierro

@miguelgfierro miguelgfierro commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Description

Moves vowpalwabbit>=9.9.0,<10 from the experimental extra to the core dependencies, ports the VW wrapper in recommenders/models/vowpal_wabbit/vw.py from a subprocess call to the python bindings, and rewrites vowpal_wabbit_deep_dive.ipynb around that class so it reads like the other model notebooks, with tuned parameters for every model.

The notebook was broken on pandas 2 (DataFrame.append no longer exists) and the pinned VW 8.x has no wheels for Python 3.10 or 3.11. The VW class existed to demonstrate usage in the notebooks but the notebook never used it, and it shelled out to a vw binary the pip package does not ship.

vowpalwabbit 9.11.2 ships wheels for CPython 3.10 to 3.14 on Linux x86_64 and aarch64, macOS and Windows, declares no runtime dependencies and is BSD-3 licensed, so it does not hold back the Python versions the package can support.

VW class

  • Runs through vowpalwabbit.Workspace, no vw executable needed. Parameters are the vw command line options as keyword arguments, so VW(q="ui", b=26) is vw -q ui -b 26. The training parameters are arguments of fit(df, epochs, learning_rate, l2), and train-only options such as q are not passed to prediction. Predictions are bit-identical to the executable.
  • New recommend_k_items(test, top_k, remove_seen) with the same signature as SAR: scores every user in the test set against every item seen during training and keeps the top k.
  • New save(path) and load(path): the model is trained into a temporary directory that goes away with the object, so this is what lets a trained model be persisted and served elsewhere. vw keeps the training options in the model file, so a loaded model predicts with its own interactions, rank or oaa settings.
  • New n_jobs: prediction splits the rows in chunks scored by a process pool, each process writing and scoring its own chunk with the C++ driver. Results are identical to n_jobs=1. Scoring 21.5M candidate pairs at 1m takes about 3 minutes single process, most of it VW parsing text examples; with 8 processes the whole 1m notebook runs in about 6.5 minutes instead of 20.
  • Only binary logistic regression binarizes the training labels; loss_function="logistic" with oaa keeps the rating labels the multiclass model needs.
  • to_vw_file() builds the input file with vectorized string operations instead of a row loop; byte-identical output, about 17x faster.
  • The example tag is the dataframe index, so a named index no longer raises KeyError: 'index'.

Notebook

EPOCHS = 20 and N_JOBS = 8 are exposed at the top. Each of the six models is model = VW(...) with its own options, model.fit(train, epochs=EPOCHS, learning_rate=..., l2=...) with its own learning rate and L2, model.predict(test), model.recommend_k_items(test, top_k=TOP_K, remove_seen=True), followed by the rating metrics (rmse, mae, rsquared, exp_var) and the ranking metrics (map_at_k, ndcg_at_k, precision_at_k, recall_at_k) computed inline; the six results are compared in one table at the end. The to_vw, run_vw, temp directory, file path, candidate set and separate scoring cells are gone. The markdown explaining -q, -b, --oaa, --rank and --lrq is kept since those are now the constructor arguments.

Tuning

The parameters come from a grid search on MovieLens 100k with the notebook's split, about 1,500 fits: epochs {1, 5, 10, 20} x learning rate {0.5 to 0.005} x L2 {0 to 1e-4} for every model, the model options b, rank, lrq and lrqdropout crossed with the same training grid, and an extension down to learning rate 0.001 and up to L2 1e-3 once the optimum reached the grid edge. Models are ordered by NDCG@10. The number of epochs is shared by all models and is the value with the best mean NDCG across them (20). The learning rate, L2 and options are chosen per model as the best NDCG subject to rmse <= 1.10: without that guard the regression models reach a slightly higher NDCG at L2 = 1e-3 by shrinking every prediction to a constant (rmse 2.77), which is not a usable rating model.

Model Options Learning rate L2 NDCG@10 before -> after rmse before -> after
Linear regression 0.01 1e-5 0.2034 -> 0.2076 1.027 -> 1.038
Linear regression w/ interaction b=28 0.01 1e-5 0.2113 -> 0.2127 1.030 -> 1.057
Multinomial regression oaa=5 0.05 0 0.0908 -> 0.1236 1.169 -> 1.099
Logistic regression 0.001 1e-3 0.1532 -> 0.2325 0.759 -> 0.757
Matrix factorization (rank) rank=20 0.05 1e-4 0.1898 -> 0.2154 1.155 -> 1.058
Matrix factorization (LRQ) lrq="ui3", lrqdropout=True 0.005 1e-6 0.0757 -> 0.1964 1.022 -> 1.040

"Before" is 5 epochs, learning rate 0.02 and L2 0 for every model.

Tests

  • tests/unit/recommenders/models/test_vowpal_wabbit.py: the six tests registered in pr_gate keep their names. New tests train real models: fit/predict, save/load round trip, recommend_k_items with remove_seen, logistic predictions in [0, 1], multiple epochs, and n_jobs producing the same recommendations as a single process; the multiclass label handling and the training parameters being fit() arguments are covered without training.
  • Smoke and functional expectations for the notebook are the values it now produces, reported from the linear regression with interaction features.
Test Size Metric Old New
test_vw_deep_dive_smoke 100k rmse / mae / rsquared / exp_var 0.985920 / 0.71292 / 0.231199 / 0.231337 1.056882 / 0.79716 / 0.116548 / 0.137686
test_vw_deep_dive_smoke 100k map / ndcg / precision / recall 0.012535 / 0.096594 / 0.097770 / 0.037612 0.122644 / 0.212703 / 0.181104 / 0.065968
test_vw_deep_dive_functional 1m rmse / mae / rsquared / exp_var 0.959885 / 0.690133 / 0.264014 / 0.264417 1.147971 / 0.903852 / -0.052669 / 0.085058
test_vw_deep_dive_functional 1m map / ndcg / precision / recall 0.004857 / 0.055128 / 0.061142 / 0.017789 0.109966 / 0.197746 / 0.185414 / 0.058883

The parameters were tuned on 100k only. On 1m they raise NDCG from 0.183 to 0.198 but cost rating accuracy (rmse 0.998 to 1.148, R2 below zero), so the 1m expectations record that trade-off rather than a 1m optimum.

All the VW tests now run in CI. The experimental marker and the skip markers are gone, and the eleven new unit tests are registered in tests/test_groups.yml; the three notebook tests were already listed there. No workflow or Dockerfile changes.

Benefit

The VW model and its deep dive work again with pip install recommenders on Python 3.10 to 3.14, with no extra or manual binary install. The wrapper is a real model class with the repository's standard interface, including top-k recommendations, parallel scoring and a trained model that can be saved and loaded back, and the notebook shows that interface, the metric calls and a tuned configuration for each model rather than file plumbing.

Risk

The VW deep dive now runs in the PR gate, which adds about 84 s to group_cpu_spark on a 4 core runner, and the 1m functional test adds about 9 min to the nightly. The unit tests are toy sized and add under a second.

vowpalwabbit is a compiled dependency whose wheels for new CPython versions have historically arrived late (3.11 to 3.13 only got wheels in 9.11.x, March 2026). Support for Python versions after 3.14 in the core package will depend on upstream publishing wheels for them.

Checklist:

  • I have followed the contribution guidelines and code style for this project.
  • I have added tests covering my contributions.
  • I have updated the documentation accordingly.
  • This PR is being made to staging branch AND NOT TO main BRANCH.

Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
…led in CI

Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
@miguelgfierro
miguelgfierro marked this pull request as draft September 4, 2026 17:59
…mend_k_items

Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
…tems

Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
@miguelgfierro miguelgfierro changed the title Upgrade the Vowpal Wabbit example to vowpalwabbit 9 Port Vowpal Wabbit to the python bindings and upgrade to vowpalwabbit 9 Sep 4, 2026
…e at the end

Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
…metrics for every model

Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
…arning

Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
@miguelgfierro
miguelgfierro marked this pull request as ready for review September 4, 2026 21:16
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
@miguelgfierro
miguelgfierro marked this pull request as draft September 5, 2026 06:10
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
Signed-off-by: miguelgfierro <miguelgfierro@users.noreply.github.com>
@miguelgfierro
miguelgfierro marked this pull request as ready for review September 5, 2026 08:08
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