Repository navigation
refactor(embed)!: emit one typed embedding column instead of two vector copies (standalone) - #2794
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
| vector_series = pd.Series(pd.arrays.ArrowExtensionArray(array), index=df.index) | ||
| error_series = pd.Series(list(errors), index=df.index, dtype=EMBEDDING_ERROR_DTYPE) | ||
| error_column = f"{output_column}_error" | ||
| return df.assign(**{output_column: vector_series, error_column: error_series}) |
There was a problem hiding this comment.
🟡 Compact results hide embedding failures
When assign_embedding_columns records a failed embedding, compact service results omit its error. compact_result_record reads only the old error fields, so clients see no failure.
Learn more
The embedding stage now represents failures in a separate string column. Service transport projects a DataFrame into compact records with compact_result_record. That projection discards most columns and looks only at top-level error or metadata['error'], so the newly emitted error cannot reach compact clients. This affects the compact result schema even when the legacy schema carries the error column.
Example: A row has embedding=None and embedding_error='RuntimeError: embedding batch failed'. Compact transport returns text and provenance without any error, leaving the client unable to distinguish a failed request from absent embeddable content.
Recommended fix: Make compact_result_record accept the configured embedding column, read its matching error column, and expose a compact error entry when populated. Test the default and custom output column with result_schema='compact'.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
This is not a regression from this PR. compact_result_record's error handling is identical to main: it reads only the top-level error / metadata["error"]. On main, the embedding error lived inside the text_embeddings_1b_v2 payload, which compact mode also never read. So compact clients didn't see embedding failures before this change either. Surfacing {output_column}_error in compact results would be a behavior change to the compact schema, so I'm leaving it for a separate PR rather than widening this one.
There was a problem hiding this comment.
This was already the case on main, so this PR doesn't introduce it. On main the embed stage puts its error inside the text_embeddings_1b_v2 payload (_error_payload → {"embedding": None, "error": {...}}). compact_result_record on main only reads row["error"] / metadata["error"] (ingestor/results.py:428, identical to this branch), so embedding failures were never shown in compact results there either. This PR only moves the error into the embedding_error column. The VDB path still fails closed on it (records.py:395). Showing embedding_error in compact results would be a behaviour change for service clients, so I'd do it in a follow-up rather than widen this PR.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…mplify embedding column assembly - build_lancedb_row now takes the vector as a required embedding= argument; the attribute fallback (extract_embedding_from_row) is removed so there is one path. - assign_embedding_columns uses a single np.concatenate path (offsets already come from per-row lengths), with a test for mixed-length vectors. - embedding_vector docstring says it converts, not normalizes. - ingestor_runtime reads embed_params.output_column directly. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ing_vector Drops the PyArrow-scalar (as_py) and to_numpy branches: graph rows reach this helper as lists (Series.tolist, to_dict, itertuples, Ray take) or NumPy arrays (iterrows), so the other branches only hedged for inputs nothing produces. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| if isinstance(value, np.ndarray): | ||
| value = value.tolist() | ||
| elif isinstance(value, tuple): | ||
| value = list(value) | ||
| if not isinstance(value, list) or not value: | ||
| return None |
There was a problem hiding this comment.
🟡 Arrow scalar embeddings block VDB uploads
When a row contains a PyArrow list scalar, embedding_vector returns None instead of its vector. to_client_vdb_records then rejects the valid row as missing an embedding.
Learn more
A graph row can carry an embedding as a PyArrow list scalar or a pandas extension array. The conversion helper previously converted these values into lists, but now recognizes only NumPy arrays and tuples. to_client_vdb_records interprets a rejected embedding as absent and raises a VDB upload error for searchable content. The same helper feeds build_lancedb_row, which silently skips rejected embeddings.
Example: For {'text': 'example', 'embedding': pa.scalar([0.5, 0.25], type=pa.list_(pa.float32()))}, the vector is rejected instead of stored as [0.5, 0.25].
Recommended fix: Restore conversion of supported PyArrow scalars with as_py() and pandas extension arrays with to_numpy() or tolist() before the nonempty-list check. Retain NumPy and tuple support.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
I couldn't find a path that reaches this. The embed stage stores the vector as a pandas ArrowExtensionArray (list<float32>). I checked pandas' row access on that column: .iloc, to_dict(orient="records") and itertuples return a Python list, and iterrows and apply(axis=1) return a numpy.ndarray. embedding_vector already handles all of those. A pyarrow.ListScalar only shows up if you index a raw pa.Array/pa.Table directly, and the record builders don't do that. I'm leaving the code unchanged. If we want to be defensive anyway, it's a one-line as_py() branch, and that's the maintainer's call.
|
Main vs this PR, bo767, 1x H100 NVL (NRB, stock config
The control is a reused run from two days earlier, and the throughput difference comes from one run per arm, so treat it as indicative. The control run recorded no memory telemetry. For memory, see the supplementary numbers below (driver PSS 45.1 → 38.7 GiB on the same H100). Supplementary: the stacked #2781 version (same change, measured on main
Rows processed and stored matched on every workload. Full details are in #2781. |
[Medium impact] The PR appears safe to merge with its explicitly intended breaking changes. SummaryThe embedding stage now writes one typed
No actionable new issues were found. Tests were not run during this review.
|
…_lancedb_rows / LanceDBWriteOperator Keep one test per behaviour of the typed embedding column contract. build_lancedb_rows and its only caller, LanceDBWriteOperator, have no importer outside tests. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class LanceDBWriteOperator(AbstractOperator, CPUOperator): |
There was a problem hiding this comment.
[P2] Account for this additional breaking surface
This deletes the importable nemo_retriever.operators.graph_ops.lancedb_write_operator module and LanceDBWriteOperator class, but the PR description’s explicit breaking-change list only covers embedding columns and configuration fields. The absence of in-repo imports does not protect downstream callers from an import failure. To keep this standalone representation change focused, could we retain and update this operator (or leave a compatibility shim)? If its removal is intentional, please at least identify the module/class removal in the PR description and release notes, with the rationale for including it here.
Resolve runtime.py conflict: keep main's embed_inference_batch_size and this PR's output_column='embedding' default. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…fragments, the driver commits once Rebased onto main after #2794 (one typed embedding column); drops the #2778 and #2781 stack. Squashes the #2785 branch (b17b65f..71183b7): Arrow-column streaming to LanceDB, worker-native fragment writes from 2 long-lived Ray Data writer actors (num_cpus=0, worker_write_rows=2048), single driver commit, fail-closed stage-error validation and fragment cleanup on failure. Graph rows are built with DataFrame.to_dict(orient='records') instead of #2778's frame_to_records. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Description
Problem. Every embedded row carries its vector twice: once as
metadata["embedding"]and once inside thetext_embeddings_1b_v2struct. Both copies travel through the Ray object store and into the driver, which doubles vector memory per row.Change. The embed stage writes one Arrow
list<float32>column,embedding(EmbedParams.output_column), plus a string columnembedding_error. Every writer goes throughassign_embedding_columns(models/inference/main_text_embed.py). Readers (common/vdb/records.py,operators/vdb.py,ingestor/results.py, and the LanceDB write and query paths) read only the configured column. Replaces the stacked #2781 (closed). It doesn't depend on #2778.Scope (breaking, no compatibility shim).
text_embeddings_1b_v2*,metadata["embedding"]on graph rows,_contains_embeddings.embedding_key,embedding_dim_column,has_embedding_column,embed_output_column.output_columnnow defaults toembeddinginstead oftext_embeddings_1b_v2.Unchanged.
_l2_normalizein float32, or vLLM pooling).vector: list<float32>[dim]schema.metadata["embedding"]handed to VDB backends.Measured (NRB bo767, 1x H100 NVL, stock config, n=1 per arm; details in the PR comment). main
8c95ad7c→ this PR:The same change measured as #2781 (stacked on #2778, bo767 n=3): driver PSS peak 45.1 → 38.7 GiB, and 24.98 → 28.20 pages/s.
Tests.
pytest tests --ignore=tests/integration: 3790 passed, 185 skipped.Port notes vs #2781 (conflict resolutions against current main)
runtime.py,operators/vdb.py,graph/ingestor_runtime.py,graph/retriever.py: currentmainlogic (query splitting/grouping from Fix long-query retrieval failures and incorrect result grouping #2788,_embedding_params,_query_text_column) is kept as is.embedding_columnis passed through.mainafter the stack (test_split_query_retrieval.py,test_nemotron35_embedder.py,test_graph_embedding_identity.py) now write the typedembeddingcolumn.Checklist
Link to Devin session: https://nvidia-cloud.devinenterprise.com/sessions/07b4aa3f6ffd42ddba417a3c9a38a168
Open in Devin Desktop: https://nvidia-cloud.devinenterprise.com/desktop/session/07b4aa3f6ffd42ddba417a3c9a38a168?variant=devin
Requested by: @jioffe502