Skip to content

fix: scale LightGBM validation data transfer - #2664

Open
Rana Singh (ranadeepsingh) wants to merge 28 commits into
microsoft:masterfrom
ranadeepsingh:copilot/issue-2294-validation-data-scaling
Open

fix: scale LightGBM validation data transfer#2664
Rana Singh (ranadeepsingh) wants to merge 28 commits into
microsoft:masterfrom
ranadeepsingh:copilot/issue-2294-validation-data-scaling

Conversation

@ranadeepsingh

@ranadeepsingh Rana Singh (ranadeepsingh) commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

What this fixes

Closes #2294.

Large sparse validation sets supplied through validationIndicatorCol were preprocessed with collect() and broadcast as Array[Row]. That required Spark to return the complete validation set to the driver, exposing users to driver result-size and heap failures.

This change removes the large validation-row collection and broadcast while preserving exact LightGBM validation and early-stopping semantics.

Design

  • Materialize every preprocessed validation row to a driver-local spool through DataFrame.foreachPartition.
  • Preserve every row and partition order; no sampling or validation sharding is introduced.
  • Broadcast only a small, versioned, authenticated stream descriptor while retaining legacy Array[Row] descriptor compatibility.
  • Stream the complete spool to each task that owns a native LightGBM validation Dataset.
  • In bulk single-Dataset mode, skip transfers on helper tasks that do not own the Dataset.
  • Accept only the first complete Spark attempt for each partition so retries and speculation cannot overwrite committed data.
  • Preserve primary failures while attaching iterator, socket, executor, Dataset, broadcast, and spool cleanup failures as suppressed exceptions.

LightGBM 3.3.5 calculates validation metrics locally, including non-decomposable metrics such as AUC. Each active native Dataset owner therefore still receives the complete validation set; changing that would alter metrics and early-stopping behavior.

Resource bounds

  • Driver copy buffers: 64 KiB.
  • Concurrent ingest or serving transfers: eight.
  • Ingest and serving executors: nine daemon threads each.
  • Socket operations and shutdown are time-bounded.
  • Temporary attempt files, committed partition files, clients, executors, iterators, broadcasts, and spool directories are cleaned on success and failure.

The semantic tradeoff remains explicit: exact validation requires O(validation data) driver disk, one complete transfer per native Dataset owner, and one complete native validation Dataset per owner.

Compatibility

  • No public estimator parameters, JVM signatures, or serialized parameter shapes change.
  • TrainingContext.validationData retains its existing type and constructor position.
  • Legacy validation broadcasts remain readable.
  • Copy, schema, estimator/model persistence, sparse vectors, streaming mode, both bulk Dataset modes, and repeated fits are covered.
  • Null validation indicators fail explicitly rather than being silently removed by SQL null filtering.

Rebase and dependency composition

  • Rebased onto current master 3fac9b20e81d8e72c877781c746af171f39477ea.
  • Pushed head: c0fe6bdd6e71440df7da9d03105f62d7be1cc5aa.
  • The original 20 commits range-diff one-for-one after the rebase. The only intentional semantic differences are the two StreamingPartitionTask conflict compositions that retain both PR fix: clean up failed LightGBM streaming datasets #2662's native Dataset ownership cleanup and this PR's closeable streamed-row handling.
  • PR fix: clean up failed LightGBM streaming datasets #2662 is merged. Its three rebased commits are declared in .pipelines/release-compat-prerequisites.txt so the Spark 4.1 replay receives those ownership helpers before this patch.

Validation

Spark 3.5 / Scala 2.12 / JDK 11

  • lightgbm/compile: passed.
  • lightgbm/Test/compile: passed.
  • lightgbm/scalastyle: 0 findings.
  • lightgbm/Test/scalastyle: 0 findings.
  • Full codegen: passed.
  • black==22.3.0 --check --extend-exclude docs/: 198 files unchanged.
  • git diff --check: passed.
  • Focused suites: 38/38 passed across:
    • LightGBMValidationDataSuite
    • ValidationDataServerLifecycleSuite
    • StreamingDatasetLifecycleSuite
    • ReferenceDatasetUtilsSuite
  • Exact authentication follow-up: 34/34 passed across ValidationDataServerLifecycleSuite and
    LightGBMValidationDataSuite.
  • Final CI-remediation sequence passed 30/30 lifecycle tests, 5/5 bounded-result validation tests,
    and a 1/1 post-suite repeated-fit test, proving the specialized Spark session no longer poisons
    the shared TestBase session.
  • Release compatibility policy suite: 64/64 passed, including a scratch-repository regression that
    replays a prerequisite rename and verifies the source path is removed.

The public scaling regression uses 256 rows, 4,096-dimensional sparse vectors with 1,024 deterministic nonzeros, two LightGBM tasks, and spark.driver.maxResultSize=128k. Two tasks retain the legacy driver-result-size failure contract while fitting Azure's two concurrent Spark task slots.

Spark 4.1 / Scala 2.13 / JDK 17

  • Replayed PR fix: clean up failed LightGBM streaming datasets #2662's three prerequisite commits in order onto live spark4.1
    06897e5b27e28d84ce7ffa33e93d7f756992d0f2.
  • Applied final head c0fe6bdd6e71440df7da9d03105f62d7be1cc5aa using the pipeline's
    release-relevant path filtering; the renamed suite exists only at its destination.
  • Ran the repository's exact release-compatibility command, root test:compile, successfully on
    JDK 17 in 683 seconds.

Azure CI follow-up

Azure build 231829385 on the old head exposed a real test configuration defect: four LightGBM workers were requested on a two-slot runner, so the first two workers occupied every slot while waiting for workers that could not start. The regression now uses two workers, preserving multi-worker and active/helper coverage without scheduler starvation.

The automated review of 18750a514d3a079409c879a17912234b70216add found that token mismatches were classified as expected disconnects. Head 2878c517eba7ef6919a6dcb9f99995c54a3b7cd3 records authentication failures as terminal for both ingest and serving, preserves the original SecurityException, and covers both shutdown states plus the real serving/close path. The review thread was answered and resolved.

Copilot reviewed final head c0fe6bdd6e71440df7da9d03105f62d7be1cc5aa with zero active or suppressed findings; all review threads are resolved.

Exact-head Azure build 232332516 was a trigger-driven pullRequest build requested for GitHub against merge commit d0d1b44393743f52177ccac173b5d5363ff21a19. It exposed three independent failures:

  • UnitTests lightgbm1: the bounded-result provider reused and then stopped the shared SparkContext, leaving later suites with a stopped cached session. Commit 47f6ac1db4e43a2ed0357eeff1e1c32253ec9c54 isolates the specialized session and restores a healthy shared session.
  • Release Branch Compatibility Check spark4.1: name-only prerequisite enumeration collapsed a rename to its destination, so the path-filtered patch omitted deletion of the source. Commit c0fe6bdd6e71440df7da9d03105f62d7be1cc5aa enumerates with rename detection disabled and adds a representative replay regression.
  • Fabric E2E: certificate token acquisition succeeded, but workspace cleanup received repeated HTTP 500 responses from the Fabric API before any product test ran. This is retained as infrastructure evidence and will be retried by the fresh exact-head build.

Fresh exact-head Azure build 232346646 validated both remediations: UnitTests lightgbm1
passed 143/143 executed tests, and the Spark 4.1 prerequisite replay plus root test:compile
passed. Every code job completed successfully. Fabric E2E again stopped before product tests
when GET https://api.fabric.microsoft.com/v1/workspaces returned HTTP 500; the log reports
zero tests run. Controlled rerun 232352794 ran against the same merge commit
5c9d0a7e47c7fde716c94a2ace420a2e98d8d0ee, whose parents are current master
3fac9b20e81d8e72c877781c746af171f39477ea and final PR head
c0fe6bdd6e71440df7da9d03105f62d7be1cc5aa. Every non-Fabric job passed again; Fabric
workspace discovery returned the same HTTP 500 and again ran zero tests.
Final cooldown build 232363074 produced the same result: all 75 non-parent/non-Fabric checks
passed, while Fabric workspace discovery failed before running any tests.

This is a target-branch infrastructure failure rather than a PR regression. Current-master builds
232301239 and 232305251 have the same sole failed job, the same workspace-listing HTTP 500,
and zero Fabric tests executed.

Managed Fabric proof

The checked-in lightgbm-streaming scenario passed independently on managed Fabric Spark
3.5.5.5.4.20260807.1 with exact head c0fe6bdd6e71440df7da9d03105f62d7be1cc5aa:

  • Loaded the supplied core, lightgbmlib 3.3.510, and LightGBM jars; class-source and native
    mappings name those exact jars.
  • Recorded SHA-256 hashes 1ae2c732..., f2b1b131..., and f2527db6... respectively.
  • Completed two public LightGBMClassifier.fit() and prediction repetitions over 4,000 rows and
    four partitions; each prediction count was 4,000.
  • Ran as Fabric application application_1787385228417_0001.
  • Downloaded driver/executor logs and deleted the unique scratch lakehouse successfully.

Residual risk

  • The implementation is bounded on driver heap but intentionally does not reduce driver disk, network, or native-worker validation size because doing so would change LightGBM semantics.
  • Formal readiness remains blocked by the baseline Fabric CI status and the repository's required
    human approval, despite exact-head managed-runtime coverage passing independently.

@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 Rana Singh (@ranadeepsingh) 👋!
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.

@ranadeepsingh
Rana Singh (ranadeepsingh) force-pushed the copilot/issue-2294-validation-data-scaling branch from fbe4a0a to b33dcc4 Compare August 18, 2026 12:49
Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 18, 2026
## Summary
Track and forcibly close active validation clients, use daemon executors with verified termination, and make spool ownership exception-safe across socket, executor, and server-start failures. Add deterministic lifecycle coverage for stalled and cancelled clients, construction failures, accept timeouts, real serving failures, and nontermination.

## Prompting Intent
Fix the resource-lifecycle blockers found in PR microsoft#2664 without changing exact LightGBM validation semantics: prevent blocked socket writes and JVM thread leaks, delete spools on every safe construction-failure path, preserve genuine serving failures, and tolerate expected task cancellation or speculation.

## Linked Sources
- Issue: microsoft#2294
- Draft PR and lifecycle review: microsoft#2664
- Related issue: microsoft#924
- Related issue: microsoft#978

## Rationale
Closing tracked sockets before executor shutdown is the only reliable way to unblock socket writes; thread interruption alone is insufficient. Spool deletion is gated on confirmed executor termination so no worker can read deleted files. Expected socket disconnects and accept polling timeouts are nonterminal, while file and generic I/O failures remain visible to await(). Injectable resource factories make every ownership transition deterministic to test without environmental port races.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ranadeepsingh

Rana Singh (ranadeepsingh) commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Lifecycle follow-up pushed at 3eaca96. Active ingest/serve sockets are now closed before executor shutdown; spool deletion requires confirmed termination; construction ownership is covered through bind, executor, and start failures; cancellation/accept timeouts remain nonterminal while real I/O failures propagate. JDK 11 compile/test-compile and both scalastyle tasks passed, Black passed, and the two targeted suites passed 13/13. Copilot automated review was requested and polled twice for 10 minutes but no review exists for this head. Azure was intentionally not triggered. Merge #2662 first, then rebase #2664 and preserve both cleanup changes.

@ranadeepsingh
Rana Singh (ranadeepsingh) marked this pull request as ready for review August 18, 2026 18:59
Copilot AI lite review requested due to automatic review settings August 18, 2026 18:59
@azure-pipelines

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

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.

Pull request overview

This PR fixes LightGBM training failures when validationIndicatorCol is used with large/sparse validation sets by eliminating the driver-side collect() + broadcast of all validation rows. Instead, it spools preprocessed validation rows to driver-local disk and streams them to executors/tasks that construct native LightGBM validation Datasets, preserving full validation semantics (each worker still receives the complete validation set).

Changes:

  • Add a driver-side ValidationDataServer that ingests per-partition validation rows via sockets into a spool directory, then serves the spool to executors via authenticated streaming.
  • Update streaming and bulk partition tasks to consume validation rows via ValidationDataServer.read(...) (iterator + explicit close) and to use the streamed row count.
  • Add lifecycle and end-to-end tests covering cleanup, persistence, bulk/stream modes, and null validation indicators.
Show a summary per file
File Description
lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/ValidationDataServer.scala New spool + stream server/descriptor format for scalable validation data transfer.
lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/LightGBMBase.scala Replaces validation collect() broadcast path with server-backed broadcast descriptor and explicit null-indicator rejection.
lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/StreamingPartitionTask.scala Streams validation rows into the shared streaming validation Dataset and closes the iterator.
lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/BulkPartitionTask.scala Streams validation rows for bulk-mode aggregation and closes the iterator.
lightgbm/src/test/scala/com/microsoft/azure/synapse/ml/lightgbm/split1/ValidationDataServerLifecycleSuite.scala New deterministic tests for server lifecycle cleanup and failure handling.
lightgbm/src/test/scala/com/microsoft/azure/synapse/ml/lightgbm/split1/LightGBMValidationDataSuite.scala New integration tests ensuring validation streaming avoids driver result-size issues and covers persistence/bulk/null-indicator behavior.

Review details

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

Suppressed comments (1)

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/ValidationDataServer.scala:689

  • Row lengths come from the network stream; a negative value other than the EndOfStream marker should be rejected explicitly. Without a guard this can throw NegativeArraySizeException (or lead to unexpected allocation behavior) instead of a clear I/O error.
          val length = input.readInt()
          if (length == EndOfStream) {
            close()
            None
          } else {
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Lite

Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 18, 2026
## Summary
Reject malformed negative row lengths in both validation ingest and executor read paths, and preserve training or await failures when broadcast and server cleanup also fail. Add deterministic malformed-frame and exception-suppression coverage.

## Prompting Intent
Resolve every exact-head Copilot review finding on PR microsoft#2664 without adding a semantics-changing frame-size cap: validate network-provided row lengths before allocation or copy, keep primary training diagnostics intact across cleanup, test both active and suppressed findings, and maintain cross-version compatibility.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Frame-length review: microsoft#2664 (comment)
- Cleanup review: microsoft#2664 (comment)
- Suppressed executor-read finding: Copilot review 4964793710 on PR microsoft#2664

## Rationale
A shared row-length decoder applies the protocol invariant consistently without imposing an arbitrary upper bound that could reject valid serialized sparse rows. Existing cleanup helpers attach broadcast and server cleanup failures to training or await exceptions, retaining actionable root-cause diagnostics while still exposing cleanup evidence. The helper is package-private and leaves public JVM and serialized APIs unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 18, 2026 20:13
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Suppressed finding from Copilot review 4964793710 is explicitly fixed in db086b5: executor-side validation row lengths now pass through the same protocol validator as ingest, rejecting negative values other than the -1 end marker before allocation. A deterministic malformed executor-stream test covers that exact path. No upper cap was invented because the protocol has no established maximum serialized-row size and a new cap could reject valid sparse rows. Both active threads were also fixed, replied to, and resolved. Local evidence: Scala 2.12 main/test compile; full patch on spark4.0 Scala 2.13 main/test compile; 17/17 lifecycle/integration tests; both scalastyle tasks; Black. Azure was not triggered. #2662 must still merge first, followed by rebasing #2664 while preserving both StreamingPartitionTask cleanup changes.

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.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/ValidationDataServer.scala:678

  • The ValidationDataServer.read(params) iterator can leak an open socket if authentication/stream setup fails before input is initialized (e.g., socket.getOutputStream, writeUTF, or flush throws). Wrap initialization in a try/catch that closes the socket on failure and only keep input as a field.
      private val socket = connect(params.host, params.port, params.timeoutMillis)
      socket.setKeepAlive(true)
      socket.setTcpNoDelay(true)
      socket.setSoTimeout(params.timeoutMillis)
      private val auth = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream))
      auth.writeUTF(params.token)
      auth.flush()
      private val input = new DataInputStream(new BufferedInputStream(socket.getInputStream))
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

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

Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 18, 2026
## Summary
Make executor validation-stream initialization exception-safe so authentication or input-stream setup failures close the connected socket. Add deterministic coverage for an authentication output failure.

## Prompting Intent
Resolve the suppressed exact-head Copilot finding on PR microsoft#2664, preserve the original setup exception, prove the socket is closed, revalidate Scala 2.12 and Scala 2.13 builds, and keep Azure validation delegated to the coordinating parent.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed review: Copilot review on commit db086b5

## Rationale
Stream initialization now uses the existing failure-preserving cleanup helper around socket configuration and authentication. This closes the socket on every pre-input failure while suppressing any close error onto the original setup exception, matching the lifecycle guarantees used elsewhere in the server.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 18, 2026 21:09
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Addressed the new-head suppressed socket-setup finding in 965f55d. ValidationDataServer.read now closes its connected socket through the existing primary-preserving cleanup helper if socket configuration, authentication output/write/flush, or input-stream construction fails. A deterministic authentication-output failure test proves the original IOException remains primary and the socket is closed. Final validation: 18/18 lifecycle/integration tests; Scala 2.12 main/test compile; the full PR patch applied cleanly and main/test compiled on spark4.0 Scala 2.13/JDK 17; both scalastyle tasks; Black. Azure remains intentionally untriggered. #2662 still merges first, then #2664 rebases while retaining both StreamingPartitionTask cleanup changes.

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.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/ValidationDataServer.scala:478

  • timeoutMillis is calculated using IngestPollTimeoutMillis as the seconds→milliseconds conversion factor. This couples the socket timeout semantics to the accept-poll constant, so changing the poll interval would silently change all timeouts. Use an explicit seconds→ms conversion instead (e.g., timeoutSeconds * 1000.0).
      val timeoutMillis = (timeoutSeconds * IngestPollTimeoutMillis).toLong
      socket.setSoTimeout(Math.max(IngestPollTimeoutMillis, timeoutMillis).min(Int.MaxValue).toInt)

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/ValidationDataServer.scala:658

  • rowCount converts a Long to Int via Math.toIntExact, which will throw a bare ArithmeticException if validation row count exceeds Int.MaxValue. Since downstream LightGBM APIs require Int row counts anyway, it would be clearer to fail with a targeted IllegalArgumentException explaining the limit.
  def rowCount(data: Broadcast[Array[Row]]): Int = {
    ValidationDataParams.fromBroadcast(data)
      .map(params => Math.toIntExact(params.rowCount))
      .getOrElse(data.value.length)
  }
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 18, 2026
## Summary
Decouple seconds-to-milliseconds conversion from the accept-poll interval and replace overflow-prone validation row-count conversion with a targeted supported-range error. Add deterministic timeout and overflow tests.

## Prompting Intent
Resolve both suppressed findings from the exact-head Copilot review on PR microsoft#2664, keep timeout semantics stable if polling changes, and make the native Int row-count limit actionable without changing valid validation behavior.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed review: Copilot review on commit 965f55d

## Rationale
An explicit milliseconds-per-second constant documents the unit conversion independently of server polling. Validation counts already must fit the downstream native Int API, so validating the range and throwing a descriptive IllegalArgumentException preserves the existing limit while replacing an opaque ArithmeticException.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 18, 2026 21:44
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Addressed both suppressed findings from the 965f55d review in bcae907. Socket timeout conversion now uses an explicit seconds-to-milliseconds constant rather than the accept-poll interval, and validation row counts outside 0..Int.MaxValue fail with a targeted IllegalArgumentException describing the downstream native limit. Deterministic tests cover 2.5 seconds -> 2500 ms and Int overflow. Final Scala 2.12 evidence: main/test compile, both scalastyle tasks, Black, 20/20 lifecycle/integration tests. Azure remains intentionally untriggered; #2662 still merges first, then #2664 rebases preserving both StreamingPartitionTask cleanup changes.

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.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/StreamingPartitionTask.scala:165

  • rows.close() can throw (socket/input close), which would mask an exception from insertRowsIntoDataset and make failures harder to diagnose. Suppress non-fatal close failures so the primary training/validation error remains primary.
    } finally {
      rows.close()
    }

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/BulkPartitionTask.scala:53

  • rows.close() can throw (socket/input close), which would mask an exception from getChunkedColumns / mergeChunksIntoAggregatedArrays. Suppress non-fatal close failures so the primary error remains primary.
      } finally {
        rows.close()
      }
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Lite

Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 18, 2026
## Summary
Use millisecond-precise ingest deadlines and preserve row-processing failures when validation iterator cleanup also fails in streaming and bulk modes. Add deterministic regression coverage.

## Prompting Intent
Resolve the active and suppressed findings from the exact-head Copilot review on PR microsoft#2664 while keeping validation semantics and cleanup guarantees intact.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Active review: microsoft#2664 (comment)
- Suppressed findings: Copilot review on commit bcae907

## Rationale
The ingest deadline now derives from the already-normalized socket timeout, retaining fractional seconds. Both validation consumers share the existing primary-preserving cleanup helper, so close errors are suppressed onto processing failures rather than replacing them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 18, 2026 22:14
SynapseML CI and others added 16 commits August 24, 2026 12:16
## Summary

Preserve configured validation socket timeouts below one second, using a one-millisecond floor and explicit regression coverage.

## Prompting Intent

Resolve the final exact-head suppressed finding on PR microsoft#2664 and ensure documented fractional timeout values behave as configured.

## Linked Sources

- GitHub issue: microsoft#2294

- Pull request: microsoft#2664

- Suppressed review on commit 2e1b523

## Rationale

Ceiling the seconds-to-milliseconds conversion avoids turning a positive fractional timeout into Java's zero/infinite timeout, while a 1 ms floor preserves valid socket semantics without silently raising user values to the internal one-second accept-poll interval.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Collapse validation serving completion and cleanup into one bounded close operation, remove per-transfer Future retention and per-partition row-count maps, preserve stream cleanup failures, and validate protocol versions and partition IDs.

## Prompting Intent
Perform an adversarial simplicity and maintainability audit of PR microsoft#2664 after its review fixes. Remove avoidable lifecycle machinery, prove disk/network/thread bounds remain explicit, preserve exact validation semantics and compatibility, and verify the required merge composition with PR microsoft#2662.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Overlapping lifecycle pull request: microsoft#2662
- Exact-head automated review on b5b1ce6

## Rationale
A single close boundary can stop acceptance, close active sockets, terminate the fixed executor, surface serving failures, and delete the spool without a separate await phase or unbounded Future queues. Semaphore ownership provides a constant-memory ingest completion barrier. Driver disk and complete per-worker network transfer remain intentionally linear because native LightGBM requires every native validation Dataset to contain the exact full validation set; sampling, sharding, or persistent Spark file distribution would change semantics or leak large files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Size the validation ingest listener backlog to the validation DataFrame partition count and add a deterministic regression that captures the requested backlog before bind.

## Prompting Intent
Resolve the exact-head Copilot finding on PR microsoft#2664 that a fixed backlog of 50 can reject simultaneous validation-partition connections when Spark schedules more than 50 ingest tasks, while preserving bounded transfer concurrency and construction cleanup.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Copilot review thread: microsoft#2664 (comment)

## Rationale
The accept loop intentionally processes at most eight transfers concurrently, so additional simultaneous connections wait in the operating-system accept queue. Passing the exact validation partition count avoids an artificial 50-connection bottleneck; the socket factory still clamps small values to the existing default and the operating system retains authority over its maximum backlog.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Project only the validation indicator column before collecting the one-row null-existence probe, and extend the public estimator regression with a feature vector larger than the configured driver result limit.

## Prompting Intent
Resolve the suppressed exact-head Copilot finding on PR microsoft#2664 that null validation-indicator detection could still return a complete wide feature row to the driver and recreate the driver-memory failure this PR removes.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed Copilot review on head 303fb96

## Rationale
The null check needs only existence, not any training columns. Selecting the boolean indicator before `head(1)` preserves the explicit null rejection semantics while bounding the returned row independently of feature width. The regression uses deterministic one-megabyte dense vectors under `spark.driver.maxResultSize=256k`; it fails before the projection and returns the intended validation error after it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Acquire the Spark serializer instance before opening the executor validation socket and add a deterministic lifecycle regression proving serializer construction failure cannot initiate a server connection.

## Prompting Intent
Resolve the suppressed exact-head Copilot finding on PR microsoft#2664 that executor iterator initialization opened its socket and input stream before serializer construction, allowing a serializer initialization failure to leak both resources.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed Copilot review on head b469be2

## Rationale
Serializer construction does not depend on the validation connection, so acquiring it before any network resource is the smallest and safest ownership ordering. Existing socket setup already closes the socket if authentication or input construction fails, and initial row decoding already closes both resources on failure. The regression listens on an ephemeral port and proves a synthetic serializer construction failure is preserved without any connection attempt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Cap validation ingest accept polling at the configured socket timeout and add a deterministic construction-path regression for a 250-millisecond timeout.

## Prompting Intent
Resolve the suppressed exact-head Copilot finding on PR microsoft#2664 that the fixed one-second accept polling interval could delay configured subsecond ingest timeouts by up to one second.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed Copilot review on head 00330a4

## Rationale
The ingest accept loop still needs periodic wakeups for completion and shutdown, but its poll must never exceed the user-configured timeout. Taking the minimum of one second and the already validated socket timeout preserves the existing low-overhead polling for ordinary values and exact subsecond behavior without adding another timer or thread.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Resize the public LightGBM validation regression from 16,000 moderately sparse rows across eight tasks to 256 wider sparse rows across four tasks, and lower the driver result limit to retain a deterministic pre-fix failure.

## Prompting Intent
Address Azure build 231779254, where coverage instrumentation made the validation regression exceed the lightgbm1 job cap, without weakening its proof that validation rows are not collected on the driver or changing production behavior.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Failed Azure build: https://msdata.visualstudio.com/b9b2accc-2d1c-45b3-9d24-0eb5d78cc47f/_build/results?buildId=231779254

## Rationale
The smaller dataset retains exact estimator, schema, persistence, copy, sparse-vector, scoring, and cleanup coverage while reducing native training and complete validation transfer work by more than an order of magnitude. Deterministically varied nonzero values prevent task-result compression from hiding the old collect path: with the legacy collect behavior each partition returns about 156 KiB and fails the 128 KiB driver limit, while the streamed implementation passes. No production logic changed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Make the stalled-client test output treat interruption after socket closure as expected cancellation and discard subsequent synthetic writes, eliminating uncaught executor-thread errors without changing production code.

## Prompting Intent
Address coverage evidence from PR microsoft#2664 showing that the passing stalled-client lifecycle test emitted an uncaught Error wrapping InterruptedException after ValidationDataServer.close called shutdownNow.

## Linked Sources
- Pull request: microsoft#2664
- Azure build: https://msdata.visualstudio.com/b9b2accc-2d1c-45b3-9d24-0eb5d78cc47f/_build/results?buildId=231799793

## Rationale
The synthetic OutputStream exists only to block until the server closes its tracked socket. Once closure is observed, an interrupt from executor shutdown is expected and the test double can complete normally. An interrupt before socket closure is still rethrown, preserving the ordering assertion. Re-interrupting the terminating synthetic worker caused expected-disconnect logging to fail or delay executor termination under isolated coverage and Spark 4.1 runs, so the consumed cancellation is intentionally not restored.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Fall back to the JVM temporary directory when the preferred working-directory validation spool cannot be created, preserve both construction failures, and make lifecycle tests deterministic under coverage.

## Prompting Intent
Resolve the exact-head suppressed review finding on PR microsoft#2664 without changing validation semantics or adding production complexity beyond an exception-safe spool-location fallback. Keep stalled-client cancellation clean and ensure the short listener polling timeout does not become a brittle client-read timeout in coverage runs.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Exact-head Copilot review on 61013c2

## Rationale
The working directory remains preferred so existing disk placement is unchanged when writable. Files.createTempDirectory provides atomic unique creation, while the standard JVM temporary location is used only after a non-fatal preferred-location failure. If both locations fail, the fallback failure remains primary and the preferred failure is suppressed for diagnosis. The idle-timeout test now separates the intended 10 ms listener poll from client reads, avoiding coverage-only timeouts without changing server behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
In bulk single-dataset mode, stream the complete validation spool only to the active task on each executor while helper tasks satisfy shared synchronization without downloading duplicate validation data.

## Prompting Intent
Resolve the exact-head suppressed review finding on PR microsoft#2664 that helper-only bulk tasks unnecessarily consumed full validation transfers. Preserve exact validation semantics, keep non-single-dataset mode unchanged, avoid new unbounded resources, and prove the public estimator path on Spark 3.5 and Spark 4.1.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Exact-head Copilot review on 263da87

## Rationale
Single-dataset mode creates one native training and validation Dataset per executor, so only the active executor task needs the complete validation stream. Helpers still decrement the existing validation preparation latch so the active task cannot hang. Non-single-dataset bulk mode continues transferring the complete validation set to every training task, preserving native semantics. A direct policy assertion is paired with a public bulk-estimator regression for both single- and per-task Dataset modes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Reduce the validation regression fixtures from four LightGBM tasks to two so every worker can enter the network-manager rendezvous on the two-slot Azure test runner.

## Prompting Intent
Rebase PR microsoft#2664 onto the current master branch and make it engineering-ready by diagnosing and fixing real product or test defects from the exact PR CI run without weakening the validation-data scaling regression.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Azure LightGBM1 job: https://msdata.visualstudio.com/b9b2accc-2d1c-45b3-9d24-0eb5d78cc47f/_build/results?buildId=231829385&view=logs&jobId=320c32d5-b58a-5840-324d-2225935ebd3a

## Rationale
The four-task fixtures deadlocked when the first two Spark tasks occupied every available slot while waiting for workers three and four to join the LightGBM topology. Two tasks are sufficient to exercise multi-worker streaming and single-Dataset active/helper behavior, and they make each legacy collected result larger, so the sparse regression still exceeds the 128 KiB driver-result limit. Reducing fixture parallelism is more reliable than attempting to replace the shared SparkContext master inside the suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Declare the three rebased commits from merged PR microsoft#2662 as release-compatibility prerequisites so the Spark 4.1 replay receives the Dataset ownership helpers before applying PR microsoft#2664.

## Prompting Intent
Rebase PR microsoft#2664 onto the current master branch and make every engineering gate reproducibly green, including the repository's Spark 4.1 release-compatibility replay.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Prerequisite pull request: microsoft#2662
- Release compatibility gate: https://github.com/microsoft/SynapseML/blob/master/pipeline.yaml

## Rationale
PR microsoft#2664 composes PR microsoft#2662's ownership-scoped validation Dataset initialization, while Spark 4.1 has not yet received that merged master change. The compatibility job applies each configured commit's first-parent patch, so all three rebased PR microsoft#2662 commits are listed in order rather than only its final GitHub merge OID. Applying the prerequisites first makes the PR patch conflict-free and preserves the exact code that passed Spark 4.1 test compilation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Terminate the release-compatibility prerequisite configuration with a standard final newline.

## Prompting Intent
Finish PR microsoft#2664 with a clean, review-ready diff after adding the Spark 4.1 compatibility prerequisites.

## Linked Sources
- Pull request: microsoft#2664
- Prerequisite pull request: microsoft#2662

## Rationale
The compatibility parser explicitly tolerates an unterminated final record, but a newline-terminated text file avoids persistent diff annotations and works consistently with standard repository tooling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Treat validation ingest and serving token mismatches as terminal protocol failures instead of expected client disconnects, and add deterministic classification and server-close regressions.

## Prompting Intent
Resolve the exact-head automated review finding on PR microsoft#2664 without changing normal cancellation, timeout, or socket-disconnect behavior.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Review finding: microsoft#2664 (comment)

## Rationale
EOF, socket closure, timeout, and shutdown interruption can be ordinary cancellation paths, but a token mismatch means the validation descriptor or client authentication is wrong. Recording that SecurityException as the terminal failure preserves the actionable cause for both ingest and serving instead of allowing a later timeout or EOF to mask it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Run the validation scaling suite on its intended 128 KiB driver-result Spark session and restore a healthy shared TestBase session after the suite completes.

## Prompting Intent
Rebase PR microsoft#2664 and make it engineering-ready by fixing the exact-head Azure UnitTests lightgbm1 failure without weakening the bounded-driver-result regression or destabilizing later split1 suites.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Azure build: https://msdata.visualstudio.com/b9b2accc-2d1c-45b3-9d24-0eb5d78cc47f/_build/results?buildId=232332516

## Rationale
SparkSession.builder().getOrCreate() reused the already-active shared TestBase SparkContext, so stopping the suite provider also stopped the context cached for later suites. Stopping shared state before the specialized suite and explicitly restoring it afterward preserves test isolation in both directions. A live configuration assertion proves the regression actually runs under the intended result-size bound instead of silently reusing a permissive context.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Enumerate release-compatibility prerequisite paths with rename detection disabled and cover replay of a prerequisite rename in a scratch repository.

## Prompting Intent
Make PR microsoft#2664 pass the Spark 4.1 compatibility check by fixing the generic prerequisite replay defect exposed by its exact-head Azure build, while preserving all three required prerequisite commits.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Azure build: https://msdata.visualstudio.com/b9b2accc-2d1c-45b3-9d24-0eb5d78cc47f/_build/results?buildId=232332516

## Rationale
Default name-only diff output collapses a rename to its destination, which causes the later path-filtered patch to omit deletion of the source path. Disabling rename detection only during path enumeration emits both paths and lets the existing literal-path patch machinery preserve the complete change. This is simpler and less error-prone than parsing name-status records or special-casing rename pairs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/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.

Review details

  • Files reviewed: 9/9 changed files
  • Comments generated: 1
  • Review effort level: Lite

## Summary`nUnwrap accept-loop execution failures before returning them to LightGBM callers, and restore the caller interrupt flag when waiting for ingest is interrupted.

## Prompting Intent`nDrive PR microsoft#2664 to merge readiness, address every current-head review finding, preserve original transport failures, and avoid false-green validation.

## Linked Sources`n- Pull request: https://github.com/microsoft/SynapseML/pull/2664`n- Review comment: https://github.com/microsoft/SynapseML/pull/2664#discussion_r3846851620`n- Tracking issue: microsoft#2294

## Rationale`nCentralized Java Future waiting in the existing socket-support utility so ExecutionException wrappers cannot hide the underlying IOException and interrupted callers retain cancellation state. Deterministic FutureTask regressions cover both paths without introducing network timing into the suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/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.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/ValidationDataServer.scala:350

  • spoolDirectory.listFiles() can return null on I/O error. Treating that as Array.empty can silently start a server with no partition files (while rowCount is non-zero), leading to confusing downstream failures and potentially incorrect/partial validation reads. Fail fast if the directory listing fails, and validate that the expected number of part-* files were produced.
      val partitionFiles = Option(spoolDirectory.listFiles()).getOrElse(Array.empty)
        .filter(_.getName.startsWith("part-"))
        .sortBy(file => file.getName.stripPrefix("part-").toInt)
      spoolTransferred = true
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

## Summary

Fail fast when the validation spool cannot be listed or does not contain one canonical file for every Spark partition.

## Prompting Intent

Drive PR microsoft#2664 to merge readiness, audit suppressed current-head review feedback, and prevent missing validation partitions from becoming a partial or confusing downstream read.

## Linked Sources

- Pull request: microsoft#2664
- Suppressed review finding on head 0c328fd
- Tracking issue: microsoft#2294

## Rationale

Isolated spool validation in a focused package-private helper to keep ValidationDataServer below the repository file-length limit. Exact file-count and contiguous-index checks reject unreadable, incomplete, malformed, or non-file partition entries before ownership transfers to the serving phase.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Addressed the current-head suppressed spool finding in b8f9f6a. Validation ingest now fails before serving if the spool directory cannot be listed or lacks the complete canonical part-0..part-N sequence. Added unreadable, incomplete, and ordered-valid regressions; the 40-test validation selection, compile/test-compile, and main/test scalastyle pass.

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/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.

Review details

  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Current-head readiness update for b8f9f6a2ad7bc34b21c9bce2e7d92d1c111cd45d:

  • 0 commits behind current master (57c799b834080681defec358925ef2a7d5d5eb95).
  • Exact-head Azure build 232663670 passed all 66 jobs, including Fabric E2E, Databricks CPU/GPU, Python LightGBM, all six LightGBM unit shards, and Spark 4.1 compatibility.
  • Current-head Copilot review generated zero findings and all review threads are resolved.

Scott Votaw (@svotaw) Brendan Walsh (@BrendanWalsh) Kirat Pandya (@kiratp) — could one of you review and provide the required approval for this current head?

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Final exact-head managed-Fabric evidence for b8f9f6a2ad7bc34b21c9bce2e7d92d1c111cd45d:

  • lightgbm-streaming passed on workspace singhrana_ws with Fabric Spark 3.5.5.5.4.20260807.1.
  • The managed application loaded all classes from the supplied jars: synapseml-core_2.12-HEAD-20260824-2222.jar, lightgbmlib-3.3.510.jar, and synapseml-lightgbm_2.12-HEAD-20260824-2222.jar.
  • SHA-256 provenance:
    • core: c455e279b1042e1f5b577066254c71db6bc99ba164ad66977c0e2122f337c352
    • native LightGBM: f2b1b13172699832594303ab4c04f3bc8fc2d24737e3e8c11d98d69a88c09272
    • SynapseML LightGBM: a817fc1a6dc144e1add12210b8c30fdbb1e507d6a5ef56b540803957eed180b0
  • Runtime topology: 2 block managers, 4 partitions, 8 executor cores; two independent fits each produced 4,000 predictions.
  • JUnit: 1 test, 0 failures. Scratch-lakehouse cleanup completed with exit code 0.

This supplements the fully green exact-head Azure build 232663670.

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.

[BUG] OutOfMemorySparkException only when including a validationIndicatorCol - LightGBMClassifier

3 participants