Skip to content

Run SubgraphSampler on Dataproc 2.2 / Spark 3.5 before image 2.0 becomes uncreatable - #738

Open
kmontemayor2-sc wants to merge 5 commits into
mainfrom
kmonte/subgraph_sampler_spark35_dataproc22
Open

Run SubgraphSampler on Dataproc 2.2 / Spark 3.5 before image 2.0 becomes uncreatable#738
kmontemayor2-sc wants to merge 5 commits into
mainfrom
kmonte/subgraph_sampler_spark35_dataproc22

Conversation

@kmontemayor2-sc

@kmontemayor2-sc kmontemayor2-sc commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator
 ## Why now

 Google blocks cluster creation on Dataproc image 2.0 on **2026-08-25**:

 > "On August 25, 2026, 1.x and 2.0 image versions will become unavailable for use in creating clusters"

 `spark_job_manager.py:111` hardcodes `2.0.47-ubuntu18` for the `use_spark35=False` path, and
 `cora_nalp_test` / `cora_snc_test` / `cora_udl_test` all take it. On that date those three e2e tests
 stop being able to create a cluster. So does any V1 SubgraphSampler user without `graph_db_config`,
 so this is not CI-only.

 No newer 2.0.x helps — the whole minor is gated, and `2.0.160` was the final release. 2.1 is already
 out of support and goes uncreatable 2026-12-31. That leaves 2.2 (Spark 3.5.3, Scala 2.12.18), which
 matches the `scala_spark35` tree.

 ## Why it isn't a one-line image bump

 Image 2.2 requires the `scala_spark35` jar, and its `TaskRunner` refused the pureSpark path outright:

 ```scala
 throw new Exception("PureSpark SGS not supported in spark35 yet.")
 ```

 The pureSpark task classes were already ported and compiling in that tree, just never wired up — and
 never executed, so nothing had validated them.

 ## What changed

 - **`TaskRunner`** — replaced the `throw` with the dispatch ported from the spark31 tree, UDL
   detection included.
 - **`SGSPureSparkV1Task`** — restored `sampleWithReplacement`, which the spark35 copy had dropped.
   It gates 3 tests and cost ~25 lines to bring back.
 - **`subgraph_sampler.py`** — `use_spark35` now defaults to `True`, so the cora tests move off image
   2.0. The `use_spark35_runner` experimental flag remains as an escape hatch; the dead 2.0 branch can
   be deleted after 2026-08-25.
 - **Nested field names in the proto casts** — see below.
 - **Tests** — migrated the 4 pureSpark suites from `scala/` into `scala_spark35/`, plus 2 new tests
   covering the `castTo*` + `.as[proto]` boundary.

 `spark_job_manager.py` has no net change: a bump to `2.2.85` is reverted in this branch. On 2.2.85 the
 Spark job dies at startup with `NoClassDefFoundError: io/grpc/Context`, and `dblp_nalp` — already on
 the spark35 jar and 2.2.19 before this branch — fails too, so the sub-minor is the variable rather
 than the ported code. 2.2.19 is on a supported minor and escaping 2.0 does not require the newest
 sub-minor. Worth bumping separately once the classpath change is understood.

 ## The nested-field bug, and why Spark 3.1 never saw it

 The `castTo*ProtoSchema` SQL renames top-level and array *columns* to proto names but left the fields
 *inside* array-of-struct elements at their internal `_`-prefixed names:

 ```sql
 struct(_neighbor_nodes AS nodes, _neighbor_edges AS edges) AS neighborhood
 --     ^ column renamed          ^ but elements keep _node_id, _condensed_node_type, ...
 ```

 This was always wrong; Spark 3.1 just couldn't see it. The difference is the scalapb-spark artifact,
 not Spark itself:

 | Tree | Artifact | Repeated-field deserializer |
 | --- | --- | --- |
 | `scala/` | `sparksql31-scalapb0_11` 1.0.0 | resolved `MapObjects` → **positional** access; names irrelevant |
 | `scala_spark35/` | `sparksql35-scalapb0_11` 1.0.4 | `UnresolvedMapObjects` → analyzer resolves **by name** |

 Under 1.0.0 the field order happened to match the proto, so output was correct by accident. Under
 1.0.4 the same SQL raises:

 ```
 [FIELD_NOT_FOUND] No such struct field `node_id` in `_node_id`, `_condensed_node_type`, `_feature_values`
 ```

 Downgrading is not an option — only 1.0.4 and 1.0.5 exist for the sparksql35 artifact.

 The fix renames nested fields via SQL `transform`, in three shared helpers on the base class so the
 five call sites cannot drift apart. Two related traps handled at the same time:

 - Bare `ARRAY()` is `ARRAY<VOID>`, which fails the same deserializer with
   `INVALID_EXTRACT_BASE_FIELD_TYPE`. Empty arrays are now
   `CAST(ARRAY() AS ARRAY<STRUCT<...>>)`.
 - `F.coalesce(col, F.array())` is left alone: `coalesce` widens `array<void>` against its sibling
   operand, so it never reaches the encoder as VOID.

 ## Why the existing tests missed it

 No pureSpark test crossed the `castTo*` + `.as[proto]` boundary — the closest stopped at
 `createSubgraph`. The two new tests build inputs with the production loaders against the checked-in
 TFRecord assets, because the hand-written mocks give `_node_features` a scalar `Double` where real
 data has `array<float>`. Both tests fail on the unfixed code with the production error.

 ## Verification

 Local:

 - `sbt "subgraph_sampler/test"` — 27 tests, 0 failures (25 before, +2 new)
 - `make unit_test_scala` (both trees), `make check_format_scala`, `make type_check`,
   `make unit_test_py PY_TEST_FILES="subgraph_sampler_test.py"` — all pass

 Real Dataproc 2.2, run 31068555994 — all four V1 pipelines succeeded end to end:

 | Pipeline | SubgraphSampler | Path |
 | --- | --- | --- |
 | `cora_nalp` | 7m28s | ported pureSpark |
 | `cora_snc` | 12m04s | ported pureSpark |
 | `cora_udl` | 7m28s | ported pureSpark |
 | `dblp_nalp` | 13m04s | GraphDB (no-regression control) |

 SGS runs faster than the ~28-30 min Spark 3.1 baseline, so this does not add timeout pressure.

 That run's `integration-e2e-test` is red for an unrelated reason: `hom_cora_sup` (a GLT pipeline that
 touches neither Dataproc nor Scala SGS) failed in `data-preprocessor` on a `tensorflow_transform` GCS
 rename returning 404 for its own temp object. It failed in the previous run too, at a different stage.
 The e2e runner raises on the first failed pipeline, so one flake masks the four successes.

 ## Not in this PR

 - **`scala/` (spark31) is untouched.** It is correct-by-accident under 1.0.0's positional semantics,
   and the tree is retired.
 - **No heterogeneous support** in the ported `TaskRunner`. This is a port, not a feature.
 - **`appendIsolatedNodesToTrainingSamples`** has a 6-vs-7-column UNION arity bug, left alone —
   unreachable, `includeIsolatedNodesInTrainingSamples` is hardcoded `false`.

kmonte and others added 3 commits August 5, 2026 16:41
Google blocks Dataproc image 2.0 cluster creation on 2026-08-25, so the
V1 SubgraphSampler must run on image 2.2 via the scala_spark35 jar. That
jar's TaskRunner threw "PureSpark SGS not supported in spark35 yet." for
the non-graphdb node-anchor-based link prediction path, which is exactly
what the cora_nalp/cora_udl e2e tests use.

- TaskRunner: replace the throw with the NALP/UDL dispatch ported from
  scala/subgraph_sampler TaskRunner (UDL detection via
  isPos/isNegUserDefinedForCondensedEdgeType).
- SGSPureSparkV1Task: restore the sampleWithReplacement feature
  (UDF, default params on the three sampling methods, SQL branches)
  that the ported tests exercise. The spark35 comment fixes are kept;
  this is not a wholesale copy of the spark31 file.
- Port the 4 pureSpark test suites (only import-path edits plus the
  SupervisedNodeClassificationTask ctor difference); test assets
  already existed in this tree.

Deliberately unported: the sample_with_replacement experimental-flag
reading in the NALP/UDL task classes (documented parity gap), and any
heterogeneous-graph support.

sbt "subgraph_sampler/test": 25 tests, 7 suites, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Google blocks Dataproc image 2.0 cluster creation on 2026-08-25;
the use_spark35=False path creates 2.0.47 clusters. Flip the
use_spark35_runner experimental-flag default to "True" so
default-configured pipelines (including the cora e2e tests) move to
Dataproc 2.2 now, while an explicit "False" remains a rollback escape
hatch until the 2.0 branch is deleted after the cutoff.

test_subgraph_sampler_for_spark now exercises the new default: the
spark35 sidecar jar is uploaded and passed alongside the 3.5 tfrecord
jar, and the cluster is created with use_spark35=True.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2.2.19 is over a year stale; 2.2.85 (2026-06-30, Spark 3.5.3) is the
newest 2.2.x listed on the Dataproc release page. Separate commit
because this line also moves SplitGenerator, which already runs on 2.2
unconditionally, so the bump can be reverted independently of the
SubgraphSampler migration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kmontemayor2-sc

Copy link
Copy Markdown
Collaborator Author

/all_test

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 17:12:42UTC : 🔄 Python Unit Test started.

@ 18:29:45UTC : ✅ Workflow completed successfully.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 17:12:42UTC : 🔄 E2E Test started.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 17:12:42UTC : 🔄 C++ Unit Test started.

@ 17:14:38UTC : ✅ Workflow completed successfully.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 17:12:43UTC : 🔄 Scala Unit Test started.

@ 17:23:46UTC : ✅ Workflow completed successfully.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 17:12:44UTC : 🔄 Integration Test started.

@ 18:44:12UTC : ✅ Workflow completed successfully.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 17:12:44UTC : 🔄 Lint Test started.

@ 17:22:21UTC : ✅ Workflow completed successfully.

kmonte and others added 2 commits August 5, 2026 22:35
This reverts commit f78347840e2c99b40a0b28cbc4b19b48eec9c1a6.

Image 2.2.85 breaks the SubgraphSampler Spark job. Every V1 e2e pipeline
failed within minutes of the cluster coming up, all with the same error:

    Exception in thread "main" java.lang.NoClassDefFoundError: io/grpc/Context

dblp_nalp fails too, and it already ran the spark35 jar on 2.2.19 before this
branch, so the sub-minor is the variable, not the ported pureSpark code. 2.2.85
evidently ships a different gRPC classpath than 2.2.19.

2.2.19 stays on a supported minor, and escaping image 2.0 before it becomes
uncreatable on 2026-08-25 does not require the newest sub-minor. Bumping it is
worth doing separately, once the classpath change is understood.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three pureSpark `castTo*ProtoSchema` functions renamed only top-level
columns to their proto names. Array-of-struct columns were passed through
carrying the sampling pipeline's internal `_`-prefixed element field names
(`_node_id`, `_src_node`, ...). The always-empty `neg_edges` / `hard_neg_edges`
were emitted as bare `ARRAY()` literals.

Both were latent bugs that `sparksql31-scalapb0_11 1.0.0` hid. In 1.0.0,
`FromCatalystHelpers.fieldFromCatalyst` wrapped repeated fields in
`MapObjects(lambda, input, protoSql.singularDataType(fd))`. The lambda variable
carried the proto's *declared* struct type, so the by-name field lookup inside
the lambda resolved against the declared names and compiled down to ordinal
access. At runtime the data was therefore read positionally and the wrong
element names never mattered. Field order happens to match the proto at every
creation site, so Spark 3.1 produced correct protos regardless.

`sparksql35-scalapb0_11 1.0.4` builds the deserializer with
`UnresolvedMapObjects` instead, deferring element typing to the analyzer.
Spark's `ResolveDeserializer` binds the lambda variable to the *actual* element
type and resolves fields by name, which fails at analysis time:

  [FIELD_NOT_FOUND] No such struct field `node_id` in `_node_id`,
  `_condensed_node_type`, `_feature_values`

A bare `ARRAY()` is `ARRAY<VOID>`, and with no declared element type the
analyzer has nothing to bind the lambda variable to there either:

  [INVALID_EXTRACT_BASE_FIELD_TYPE] Can't extract a value from
  "lambdavariable(MapObject, NullType, false, 347)" ... but got "VOID"

This is why cora_nalp, cora_udl and cora_snc all die in SubgraphSampler on
Dataproc 2.2 while dblp_nalp passes -- dblp takes the GraphDB path, whose SQL
already aliases nested fields to the proto names. Downgrading is not an option:
only 1.0.4 and 1.0.5 exist for the `sparksql35` artifact, and both postdate the
change.

Rewrite the element field names to the proto names at the cast boundary with
`transform`, and give the empty edge arrays an explicit element type. The
rewrite lives in three shared helpers on `SGSPureSparkV1Task` so the five call
sites across the three cast functions cannot drift apart. `transform` is
NULL-safe, so the UNION branches that emit `NULL` for their neighbor arrays
still deserialize to empty sequences. Creation sites and intermediate joins keep
their `_`-prefixed convention.

The legacy `scala/` (Spark 3.1) tree is deliberately left alone: 1.0.0's
positional semantics make the same code correct there, and it is being retired.

No pureSpark test previously crossed the `castTo*` + `.as[proto]` boundary,
which is why this reached production, so add two regression tests. The
RootedNodeNeighborhood one uses the production loaders against the checked-in
TFRecord assets rather than this suite's mock fixtures, since those mocks use
scalar/double features that the proto encoder rejects for unrelated reasons.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kmontemayor2-sc

Copy link
Copy Markdown
Collaborator Author

/all_test

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 03:29:10UTC : 🔄 Python Unit Test started.

@ 04:33:21UTC : ✅ Workflow completed successfully.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 03:29:12UTC : 🔄 Scala Unit Test started.

@ 03:41:00UTC : ✅ Workflow completed successfully.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 03:29:14UTC : 🔄 C++ Unit Test started.

@ 03:31:19UTC : ✅ Workflow completed successfully.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 03:29:15UTC : 🔄 Integration Test started.

@ 05:04:55UTC : ✅ Workflow completed successfully.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 03:29:19UTC : 🔄 Lint Test started.

@ 03:38:23UTC : ✅ Workflow completed successfully.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

GiGL Automation

@ 03:29:21UTC : 🔄 E2E Test started.

@ 04:44:37UTC : ❌ Workflow failed.
Please check the logs for more details.

@kmontemayor2-sc kmontemayor2-sc changed the title Kmonte/subgraph sampler spark35 dataproc22 Run SubgraphSampler on Dataproc 2.2 / Spark 3.5 before image 2.0 becomes uncreatable Aug 6, 2026

@yliu2-sc yliu2-sc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we actually need all those tests? I think AI added a lot of tests which weren't there before.

@kmontemayor2-sc

Copy link
Copy Markdown
Collaborator Author

Do we actually need all those tests? I think AI added a lot of tests which weren't there before.

I think AI added the tests s.t. it could do dev here. (e.g. test locally).

I think they're probably useful as such

@kmontemayor2-sc
kmontemayor2-sc marked this pull request as ready for review August 7, 2026 21:04
@kmontemayor2-sc
kmontemayor2-sc added this pull request to the merge queue Aug 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Aug 8, 2026
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