[FLINK-40346][model-triton] Preserve null array elements when deserializing Triton responses - #28947
Conversation
|
@dianfu thanks for the assignment, PR is up. Red/green verified against this commit; details in the description. |
…lizing Triton responses deserializeArrayFromJson built results with primitive backing arrays and read elements with Jackson's coercing accessors, so a JSON null became 0, false, or the literal string "null". GenericArrayData#isNullAt returns false for every position of a primitive array, so the null could not be represented even with a check in place. Scan for a null element first and keep the existing primitive path untouched when there is none. When a null is present, build a boxed array and delegate each element to deserializeFromJson, which already maps a null node to null. Reject nested array element types so a null cannot widen the accepted types, and reject a null against a NOT NULL element type rather than violating the declared output schema. This closes an asymmetry in the same class: serializeArrayToJsonArray already emitted addNull(), so a null did not survive a serialize/deserialize round trip. Generated-by: Claude Code (Opus 5)
| "Received a null array element but the declared element type is NOT NULL: %s", | ||
| elementType); | ||
|
|
||
| Object[] array = new Object[size]; |
There was a problem hiding this comment.
Allocating Object[] violates GenericArrayData's requirement that boxed primitive arrays retain their concrete component type. For example, nullable ARRAY<INT> advertises Integer[], but Flink's external conversion fast path returns this underlying Object[]; generated or user code casting it to Integer[] can then fail with ClassCastException.
| "Received a null array element but the declared element type is NOT NULL: %s", | ||
| elementType); | ||
|
|
||
| Object[] array = new Object[size]; |
There was a problem hiding this comment.
It's also good to call toTritonDataType(elementType) before the loop to reject unsupported types.
…e component type GenericArrayData requires boxed arrays to retain their concrete component type: ArrayObjectArrayConverter#toExternal returns the backing array directly, so a plain Object[] failed the caller's cast to Integer[]. Allocate via LogicalTypeUtils#toInternalConversionClass instead, and reject unsupported element types up front via toTritonDataType. Generated-by: Claude Code (Opus 5)
74bfd43 to
936c053
Compare
|
Thanks @dianfu — you're right on both counts, and the first one is reachable. I reproduced it: with an The Fixed in Object[] array =
(Object[])
Array.newInstance(
LogicalTypeUtils.toInternalConversionClass(elementType), size);Two tests cover it:
Your second suggestion is adopted as well — Also rebased onto master and pushed the fix as a separate commit, so the delta against your review is isolated in
|
dianfu
left a comment
There was a problem hiding this comment.
@SEPURI-SAI-KRISHNA Thanks for the update! LGTM.
|
@dianfu Thanks for your time and review |
What is the purpose of the change
TritonTypeMapper#deserializeArrayFromJsonsilently corrupts null elements in array-typedTriton responses. A JSON
nullinside an array is read back as0,false, or the literalfour-character string
"null", withisNullAt()reportingfalseat that position — so thesubstituted value is indistinguishable from a real prediction downstream.
Two things combine to cause it:
int[],double[],boolean[], ...) andfills it with Jackson's coercing accessors.
NullNode.asInt()returns0,asBoolean()returnsfalse,asDouble()returns0.0, andasText()returns the string"null". No branch checksisNull().GenericArrayData#isNullAtisreturn !isPrimitiveArray && ((Object[]) array)[pos] == null;, so a primitive-backedGenericArrayDatareportsisNullAt() == falseat every position by construction. A nullcannot be represented in that shape at all.
Measured on unpatched master:
ARRAY<STRING>["a", null, "b"]isNullAt(1) == false,getString(1) == "null"(4 chars)ARRAY<INT>[1, null, 3]isNullAt(1) == false,getInt(1) == 0ARRAY<DOUBLE>[1.5, null]getDouble(1) == 0.0ARRAY<BOOLEAN>[true, null]getBoolean(1) == falseThis is a correctness bug rather than a crash, which is what makes it worth fixing: an inference
result of
0orfalselooks like a legitimate model output, so the corruption propagatessilently into user queries.
It is also an asymmetry within the same class.
serializeArrayToJsonArrayalready emitsaddNull()for a null element, and the scalar pathdeserializeFromJsonalready returnsnullfor a null node. Only the array deserialization path drops the information, so a null does not
survive a serialize/deserialize round trip.
Brief change log
deserializeArrayFromJsonscans for a null element first. When there is none — the common case— the existing primitive fast path runs completely unchanged.
deserializeNullableArrayFromJsonbuilds a boxedObject[]anddelegates each element to the existing
deserializeFromJson, which already maps a null node toa Java
nulland already covers every element type the primitive path supports, including theFloatTypeisNumber()special case.element cannot change which element types are accepted.
NOT NULLelement type is rejected with a clear message rather than writteninto an array whose declared type forbids it.
Verifying this change
This change adds tests and can be verified as follows:
TritonTypeMapperTest#testDeserializeArrayWithNullStringElementTritonTypeMapperTest#testDeserializeArrayWithNullNumericElementsTritonTypeMapperTest#testDeserializeArrayWithNullBooleanElementTritonTypeMapperTest#testNullElementSurvivesSerializeDeserializeRoundTripTritonTypeMapperTest#testDeserializeArrayRejectsNullForNotNullElementTypeTritonTypeMapperTest#testDeserializeArrayWithoutNullsIsUnchangedRed/green verified against this exact commit:
Tests run: 14, Failures: 5— the five null cases fail,reading back exactly the
0/false/"null"values tabulated above.TritonTypeMapperTest14/14, and the wholeflink-model-tritonmoduleTests run: 99, Failures: 0, Errors: 0, Skipped: 0.testDeserializeArrayWithoutNullsIsUnchangedpasses in both directions by design — it pins theprimitive fast path so a future change cannot quietly route null-free payloads through the boxed
branch.
Does this pull request potentially affect one of the following parts:
@Public(Evolving): nopayloads keep the existing primitive arrays and gain one
isNull()pass over a node list thatis iterated immediately afterwards. Payloads containing a null take a boxed array, which is
the only shape that can represent the value correctly.
Kubernetes/Yarn, ZooKeeper: no
Documentation