Skip to content

fix: generate TabularSHAP samples lazily - #2768

Merged
Rana Singh (ranadeepsingh) merged 6 commits into
microsoft:masterfrom
leninworld:fix-tabular-shap-buffering-issue-2206
Oct 15, 2026
Merged

Rana Singh (ranadeepsingh) merged 6 commits into
microsoft:masterfrom
leninworld:fix-tabular-shap-buffering-issue-2206

Conversation

@leninworld

@leninworld Lenin Mookiah (leninworld) commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Related Issues/PRs

Related to #2206. The reported eager-buffering failure is reproduced with synthetic notebook-shaped data; the reporter's original notebook run has not been independently reproduced.

What changes are proposed in this pull request?

Generate TabularSHAP sample rows incrementally instead of materializing a sample array for each instance/background pair. This avoids the large buffers created when Spark fuses the broadcast join and array expansion.

  • Contributor's change: use Dataset.mapPartitions with a row encoder. Preserve the sampler, sample budget, weights, coalition vectors, column order, target-class handling and public API.
  • Maintainer follow-up: preserve nulls recursively inside background arrays, maps and structs; add a real Spark sampling-path regression and public nested-null regressions; explain remaining scoring cost in the notebook. The submitted row encoder otherwise rejected valid nested background nulls.
  • Preserve the contributor's commits. Merge the current target rather than rewriting the fork branch.

The lazy iterator test remains as a separate consumption check. The added Spark-path test executes createSamples with two broadcast background rows and checks encoding before the first result, without heap-size or timing assertions.

How is this patch tested?

  • I have written tests (not required for typo or doc fix) and confirmed the proposed feature/bug-fix/change works.

Maintainer validation: Windows amd64, JDK 11, Spark 3.5.0, Scala 2.12.17.

Command/check Result
sbt core/compile core/Test/compile Passed
sbt core/scalastyle core/Test/scalastyle Passed, zero errors/warnings
sbt "core/testOnly com.microsoft.azure.synapse.ml.explainers.split1.TabularSHAPExplainerSuite com.microsoft.azure.synapse.ml.explainers.split1.VectorSHAPExplainerSuite" 13 passed, none skipped
sbt core/codegen Passed; generated Python wrapper import/public-setter smoke passed with PySpark 3.5.0
black --check --workers 2 --extend-exclude 'docs/' . (22.3.0) Passed, 209 files unchanged; explicit asyncio loop initialization used for the host Python
Edited notebook JSON parsing Passed

Fail-before/pass-after: the nested-null public-transform test fails on the submitted head at the row encoder and passes after the follow-up. Replaying the original eager createSamples implementation makes the Spark-path regression fail: it encodes 20,000 feature values before returning one sample. The lazy implementation passes. Scalar-null/non-null controls and genuine incompatible-type rejection were also checked.

Memory and runtime: a local public-Spark harness used 13 mixed string/integer features, 5 explained rows, 100 background rows, 5,000 samples and 8 local workers. With a 1 GiB heap, the pre-PR sampling implementation terminates with OutOfMemoryError: Java heap space; the lazy path completes all 5 rows in 21.7 seconds, with finite explanations and the expected baseline/sum. This is a replay of the old sampling method with unchanged shared code, not a full old-release installation.

With a 4 GiB heap, the same workload's two warm runs were 10.44/10.16 seconds before and 11.38/10.33 seconds after (about 5% slower on their median); cold runs were 17.72/20.91 seconds. A smaller 20-background-row workload showed about 17% warm overhead. This is a measured memory/runtime trade-off, not a universal speedup. Maintainer changes add schema work once per plan, not per sample.

The contributor separately reported Docker/Linux fail-before/pass-after runs on the original implementation and released 1.0.4/1.1.3, plus deterministic SHAP parity checks:

Contributor's TabularSHAP fail-before and pass-after Docker validation

CI for 9e66546: Azure build 240826424, triggered with /azp run, completed against merge 856d25b (parents: current master d51118a and this head). Published results contain 3,746 passed, 22 skipped, zero failed. Both added regressions ran and passed in TabularSHAP's 8/8 test run; VectorSHAP passed 5/5, and Databricks CPU/GPU notebook tests passed 43/43 and 4/4.

Azure reports partiallySucceeded because three non-gating dependency-cache tasks encountered SSL errors in UnitTests misc; that job's 502 tests and test publishing passed. All 22 skipped test cases also skipped in the successful prior master build 240603209. The website coverage publisher reported no coverage file; website tests passed. Fabric E2E and Internal compatibility were intentionally excluded by existing fork-PR conditions.

GitHub compile/style checks, website build/tests, dependency review and both configured CodeQL analyses passed. Website deployment is intentionally skipped for PRs. The aggregate CodeQL check is neutral because a separate master API-upload configuration is absent on this PR. Current-head automated review has zero open or suppressed findings; its earlier CI-pending note is now superseded by these results.

Spark Connect and port-runtime validation were not performed locally. VectorSHAP's existing sampling implementation is outside this focused change. Contributor sign-off and required human review remain outstanding.

Does this PR change any dependencies?

  • No. You can skip this section.
  • Yes. Make sure the dependencies are resolved correctly, and list changes here.

Does this PR add a new feature? If so, have you added samples on website?

  • No. You can skip this section.
  • Yes. Make sure you have added samples following below steps.
  1. Find the corresponding markdown file for your new feature in website/docs/documentation folder.
    Make sure you choose the correct class estimators/transformers and namespace.
  2. Follow the pattern in markdown file and add another section for your new API, including pyspark, scala (and .NET potentially) samples.
  3. Make sure the DocTable points to correct API link.
  4. Navigate to website folder, and run npm start to make sure the website renders correctly.
  5. Don't forget to add <!--pytest-codeblocks:cont--> before each python code blocks to enable auto-tests for python samples.
  6. Make sure the WebsiteSamplesTests job pass in the pipeline.

Copilot AI balanced review requested due to automatic review settings October 14, 2026 17:28
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@github-actions

Copy link
Copy Markdown

Hey Lenin Mookiah (@leninworld) 👋!
Thank you so much for contributing to our repository 🙌.
Someone from SynapseML Team will be reviewing this pull request soon.

We use semantic commit messages to streamline the release process.
Before your pull request can be merged, you should make sure your first commit and PR title start with a semantic prefix.
This helps us to create release messages and credit you for your hard work!

Examples of commit messages with semantic prefixes:

  • fix: Fix LightGBM crashes with empty partitions
  • feat: Make HTTP on Spark back-offs configurable
  • docs: Update Spark Serving usage
  • build: Add codecov support
  • perf: improve LightGBM memory usage
  • refactor: make python code generation rely on classes
  • style: Remove nulls from CNTKModel
  • test: Add test coverage for CNTKModel

To test your commit locally, please follow our guild on building from source.
Check out the developer guide for additional guidance on testing your change.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new RDD-based path breaks Spark Connect compatibility and violates repository requirements.

1 open finding
What changed in this PR

Makes TabularSHAP sample generation lazy to avoid executor heap exhaustion.

Changes:

  • Replaces array-based sampling with partition-level lazy iteration.
  • Preserves nullable background values.
  • Adds regressions for laziness and null handling.
File Description
TabularSHAP.scala Implements lazy sample generation.
TabularSHAPExplainerSuite.scala Tests lazy generation and nullable backgrounds.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread core/src/main/scala/com/microsoft/azure/synapse/ml/explainers/TabularSHAP.scala Outdated
Copilot AI balanced review requested due to automatic review settings October 14, 2026 18:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The laziness test covers only a helper and does not protect the production transformer path from rematerialization.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 14, 2026 20:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The nullable-background regression can pass vacuously when no results are returned.

1 open finding
Previously missed (1)

In code that hasn't changed since last review

Medium severity Test does not assert expected row count

core/​src/​test/​scala/​com/​microsoft/​azure/​synapse/​ml/​explainers/​split1/​TabularSHAPExplainerSuite.scala:150

This regression passes vacuously if the nullable-background path returns no explanations, because foreach performs no assertions on an empty result. Assert the expected row count before validating the values so the test proves all three input rows survive the new expansion path.

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 14, 2026 21:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No blocking issues remain, and reported targeted tests, parity checks, and memory-constrained validation support the focused change.

1 open finding

🧠 Review effort: Balanced

## Summary
Merge master d51118a to validate the contributor branch against the current target.

## Prompting Intent
Improve microsoft#2768 with focused, verified follow-ups while preserving the contributor's implementation and history.

## Linked Sources
- Contribution: microsoft#2768
- Target: microsoft@d51118a

## Rationale
Use a merge rather than rewriting the contributor's four commits. This imports only already-landed workflow pin updates; no new workflow or dependency change is proposed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2765695e-c815-4211-ad89-e0be56141b31
## Summary
Keep the contributor's lazy Dataset sampling and recursively relax nested sample nullability. Add public nested-null regressions and an execution-path test that observes bounded encoding before the first sample. Explain sampling cost and null handling in the tabular explainer notebook.

## Prompting Intent
Improve microsoft#2768 with meaningful, focused changes without bloating the contribution. Verify correctness and measure performance trade-offs rather than assuming a plausible memory fix is sufficient.

## Linked Sources
- Contribution: microsoft#2768
- Reported problem: microsoft#2206
- Production-path regression finding: microsoft#2768 (comment)
- Local review of 1ac2df5: public array/map/struct background-null encoding regression

## Rationale
Only sample schema construction changes in production; sampler behavior, public signatures, sample budgets, weights and column order stay intact. The execution-path test fails with 20000 encoded values on the original eager UDF without requiring an OOM or timing threshold. The nested regression fails on the submitted PR and passes with recursive nullability. Preserve the helper test as a separate iterator-consumption check.

Core compilation, main/test Scalastyle, all 13 targeted SHAP tests, core code generation, pinned Black and generated Python imports pass. A 1 GiB notebook-shaped synthetic workload OOMs with the old sampling implementation and completes in 21.7 seconds with lazy sampling. With sufficient heap, representative warm time is about 5 percent slower, an explicit trade-off for bounded expansion memory rather than a claim of universal speedup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2765695e-c815-4211-ad89-e0be56141b31
@ranadeepsingh

Copy link
Copy Markdown
Collaborator

/azp run

Copilot AI balanced review requested due to automatic review settings October 15, 2026 08:31
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@ranadeepsingh

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Current-head CI remains pending for the changed Spark sampling and row-encoding path despite reported successful local validation.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced

@ranadeepsingh

Copy link
Copy Markdown
Collaborator

Thanks Lenin Mookiah (@leninworld) for fixing the eager sample buffering! Your lazy sampling path avoids an out-of-memory failure in a synthetic workload shaped like #2206. I have kept the issue linkage conditional because I have not independently reproduced the reporter's original notebook.

I preserved your commits and lazy sampling design, merged the current target without rewriting your branch, and added 9e66546:

  • Make the sample encoder's nested struct/array/map types nullable so valid background nulls remain supported.
  • Add a real Spark sampling-path regression and a public-transform nested-null regression.
  • Add a short notebook note about streaming's memory benefit and remaining model-scoring cost.

Both regressions have fail-before/pass-after evidence. The old sampling method replay ran out of heap at 1 GiB; the lazy path completed. At 4 GiB, two warm runs showed about 5% median overhead at the larger workload and 17% at the smaller one. The description records this as a measured memory/runtime trade-off, not a universal speedup.

Azure build 240826424, triggered with /azp run, has finished for this head's verified merge with master: 3,746 passed, 22 pre-existing skips, zero failed. Its partiallySucceeded status is from non-gating cache SSL warnings; the affected job's tests passed. Published TabularSHAP results confirm both new regressions passed:

  • TabularSHAP createSamples streams encoded rows before expanding the sample budget
  • TabularKernelSHAP preserves nulls inside background features

All 13 TabularSHAP/VectorSHAP tests passed. Required GitHub checks pass, and the current-head automated review has no open or suppressed findings. Its CI-pending note is now superseded by the completed build.

Please review these follow-ups and sign off if they fit your intent. If they do not fit your vision for the PR, feel free to revert my commit, or let me know and I will revert it. Contributor sign-off and required human review are still outstanding; I have not merged the PR.

@ranadeepsingh
Rana Singh (ranadeepsingh) merged commit eca68f8 into microsoft:master Oct 15, 2026
80 checks passed
@leninworld

Lenin Mookiah (leninworld) commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks Lenin Mookiah (Lenin Mookiah (@leninworld)) for fixing the eager sample buffering! Your lazy sampling path avoids an out-of-memory failure in a synthetic workload shaped like #2206. I have kept the issue linkage conditional because I have not independently reproduced the reporter's original notebook.

I preserved your commits and lazy sampling design, merged the current target without rewriting your branch, and added 9e66546:

  • Make the sample encoder's nested struct/array/map types nullable so valid background nulls remain supported.
  • Add a real Spark sampling-path regression and a public-transform nested-null regression.
  • Add a short notebook note about streaming's memory benefit and remaining model-scoring cost.

Both regressions have fail-before/pass-after evidence. The old sampling method replay ran out of heap at 1 GiB; the lazy path completed. At 4 GiB, two warm runs showed about 5% median overhead at the larger workload and 17% at the smaller one. The description records this as a measured memory/runtime trade-off, not a universal speedup.

Azure build 240826424, triggered with /azp run, has finished for this head's verified merge with master: 3,746 passed, 22 pre-existing skips, zero failed. Its partiallySucceeded status is from non-gating cache SSL warnings; the affected job's tests passed. Published TabularSHAP results confirm both new regressions passed:

  • TabularSHAP createSamples streams encoded rows before expanding the sample budget
  • TabularKernelSHAP preserves nulls inside background features

All 13 TabularSHAP/VectorSHAP tests passed. Required GitHub checks pass, and the current-head automated review has no open or suppressed findings. Its CI-pending note is now superseded by the completed build.

Please review these follow-ups and sign off if they fit your intent. If they do not fit your vision for the PR, feel free to revert my commit, or let me know and I will revert it. Contributor sign-off and required human review are still outstanding; I have not merged the PR.

Thanks again, Rana Singh (@ranadeepsingh), for the follow-up. I independently rechecked 9e66546 in Linux amd64 Docker with Java 11.0.32, Spark 3.5.0, and Scala 2.12.18, and the additions fit the intent of the PR:

  • TabularSHAPExplainerSuite passed 8/8 and VectorSHAPExplainerSuite passed 5/5. Core compilation, test compilation, packaging, and both Scalastyle checks also passed.
  • Without recursive nullability, the nested-null regression fails with The 0th field 'value' of input row cannot be null. With the original array/explode sampling path, it instead produces an incorrect base value of approximately 0 rather than 10.
  • Replaying the original eager createSamples implementation makes the new Spark-path regression fail after encoding 20,000 feature values before returning its first sample.
  • With a 1 GiB driver heap and 16 cores, the notebook-shaped synthetic run—5 explained rows × 100 background rows × 5,000 samples—completed all five rows in 77.9 seconds without an out-of-memory error. Fully enumerated parity cases matched the pre-PR implementation within 3e-21.
    The recursive nullability correction, complementary Spark-path coverage, and documentation updates all align with the intended bounded-memory change. Thank you for strengthening the implementation and its regression coverage.

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.

4 participants