Repository navigation
Conversation
… pass, not in a loop before it (refs #318) json_bulk_dumps and json_bulk_dumps_tagged walked every value of every row in Python before json.dumps walked the rows again. The rows are now written once and the checks come from what that pass shows: the text (a surrogate escape, an integer token of 2**63 and above) and a census of the types of the keys and values in C. The exact per-row walk (_check_rows) is the reference and runs only when a value may be bad, and when json.dumps itself refuses the rows, so the exception type and message for the first bad row in row order are the old loop's. Fuzzed against the old code: 60000 cases, no difference. JVM GraphBatch.create_vertices: classify_vertex_rows decides the shapes by type census instead of a json_bulk_scalar_ok call per value; a scalar the text would change (NaN, integer beyond 64 bits, lone surrogate, subnormal) now sends its own chunk through the property-matrix path, as a tagged chunk already did. The untagged path now also refuses a subnormal, as the tagged one did. NativeGraphBatch: the dead _create_vertices_json_bulk is deleted, and the row type check is a C census. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed query (refs #318) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…per property on both wheels (refs #337) JVM: a Java float[] was no vector candidate, so classify_vertex_rows gave up on the first row and the whole load went through _to_java_property_matrix (convert_python_to_java per value, one Object[] per row, one Object[][] per call): 59 to 61 percent of the wall of the e2 and l3d loads in the untimed ingest profile of 26.11.1. A float32 numpy array on an undeclared property took the same way. take_vector_columns (type_conversion) now picks the columns: declared ARRAY_OF_FLOATS properties as before plus Java float[] and double[], and any other property whose value in every row is a float32 array or a Java float[]; the stored value of those is a float[] on both paths. The rest rows are rebuilt from columns by C iterators when the rows share their keys. Native: the float32 arrays of a row went through json.dumps with a Python hook (_gb_default, _default) per row, after a _prep walk of every value, one ["B", offset, count] tag per row parsed back in Java, and np.concatenate of one small array per row (29 to 31 percent prep in the same profile). encode_vertex_args writes the other properties with one json.dumps (no hook when they are plain scalars) and the matrices are the front of the side buffer, a ctypes view with no copy, described by one "vecs" entry that NativeExtras.withVectors turns into one float[] per row. The old per-row tag stays for edge properties and for a column that does not qualify. Tests: both wheels pin the stored bits and the split; the JVM wheel counts the JPype crossings and the property-matrix fallbacks; the native wheel counts the ctypes crossings and the codec's per-row hooks. The existing native test that expected a tag per vertex row now expects the matrix. The Java is not compiled locally: CI builds the native image. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ound the whole batch (refs #337) codec._prep is counted by name, so it also counted the calls the batch makes outside create_vertices: creating the batch (extras._enc of its options runs the original _prep, whose recursion goes through the patched name, once per option) and close() (encode_gb_args({}) once). That is 2 calls, matching the Free leg; they do not depend on the rows. The create_vertices path itself calls neither _prep nor a hook (take_vector_columns, _plain_rows and one json.dumps). The first test now takes the hook counts and the crossings around the create_vertices call (gb_default and default exactly 0, prep at most one per crossing), and a new test runs 250 rows in three chunks and 2,500 rows in three chunks with the same bounds: the old path called the hooks per row. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #330 (
perf/318-single-pass-validation): this branch is cut from its head, because both PRs editclassify_vertex_rows,split_float_vectorsandGraphBatch.create_vertices. The base isrelease/26.11.1as for the other stacked PRs, so the diff here also shows the two commits of #330 until it merges. Merge order: #330 first.Closes #337
I searched first: no open PR or issue does this. Issue #220 and PR #221 (JVM
float[]as a candidate intype_conversion.pyonly, closed on 2026-11-05 as not planned after two GRAY timed runs) are the closest. This PR is the same idea for the JVM wheel plus the part #221 did not touch (a float32 array on a property that is not declared, and the whole native encoding), with no speed gate: the issue says every glue drawback gets fixed.What I found (step 1, read from the code at
26.11.1and at the #330 head)GraphBatch.create_verticeson the JVM wheel, per value type, before this change:ARRAY_OF_FLOATSfloat[](to_java_float_array)is_vector_candidateislistorndarray),classify_vertex_rowsreturnsNone, the whole input goes through_to_java_property_matrixfloat32split_float_vectorstakes it as one float32 matrix per chunkkey in declaredis the only test), whole input through_to_java_property_matrixfloat64_to_java_property_matrix, stored as a list of doubles_to_java_property_matrix(one boxeddouble[]per row)None, missing_to_java_property_matrixA correction to the hypothesis in #337: the 26.11.1 scan in
_create_vertices_json_bulkdoes not read every row for these shapes. It returnsNoneat the first Javafloat[]it meets, which is the first row, so it costs one row. The cost is in_to_java_property_matrix.Where the 59 to 61 percent go.
_to_java_property_matrixruns once for the whole input (not per chunk) and, for each row, does a Python loop over the properties (_to_java_varargs:convert_python_to_javafor every value, about ten type tests each),jpype.JArray(jpype.JObject)(row)(JPype converts the 2K-element list: a JavaStringper key, a boxedLongorDoubleper scalar), and finally assigns the NObject[]one by one into anObject[][]. In the untimed 26.11.1 profile (cellse2andl3d, variantcv) the phase counters do not cover this function:boundary(the engine'screateVerticesalone) is 16.5 to 20.2 percent, the harness's own row building (caller, oneto_java_float_arrayper row) 21.5 to 22.8 percent, and the remainder,rest, is 59.2 and 60.6 percent. So the split inside_to_java_property_matrixis read from the code, not timed: I did not time its parts separately. Peak memory is also bigger on that path, because the Python lists, theJArraywrappers and the JavaObject[]of the whole input exist at once; the profile has no memory row for these two shapes, so the post1 A/B should take it (commands below).NativeGraphBatch.create_verticeson the native wheel (the library does not look at declarations;to_java_float_arrayreturns a float32 ndarray there, so there is no Java array):encode_gb_args:_prep(args)walks every value of every row in Python,json.dumpscalls the hook_gb_defaultthen_defaultfor every array (about a dozenisinstancetests, oneascontiguousarrayand one append per row), one{"\u0001":["B",offset,count]}tag per row is written and parsed back in Java (aHashMap, anArrayListand afloat[]per row),np.concatenatejoins one small array per row, thentobytes()copies it again. Plus a[r or {} for r in chunk]per row. This is the 29.0 and 31.3 percentprepof the profile.tolist()/json.dumpsWhat changed (step 2)
src/arcadedb/type_conversion.py:take_vector_columns(rows, declared, undeclared=False)is the one split of both wheels. A declared column qualifies as before and now also with a Javafloat[]ordouble[]. Withundeclared=Trueany other property qualifies when its value in every row is a float32 numpy array or a Javafloat[]of one length (the stacked column must come out as float32, so the stored value, afloat[], is what the property-matrix path stored; a float64 array, a list or a Javadouble[]on an undeclared property keeps the old path and its doubles).is_vector_candidateandclassify_vertex_rowsknow the Java arrays (_java_vector_arrays, resolved once the JVM runs,()on the native wheel)._without_keysrebuilds the rows without the vector columns from columns by C iterators (itemgetter,zip,dict) when the rows share their keys in one order, and by the dict comprehension otherwise.split_float_vectorskeeps its signature and return value (insert_manyuses it with the declared columns only, soinsert_manynow also takes a Javafloat[]column through the matrix).src/arcadedb/graph_batch.py:_create_vertices_json_bulkasks for the undeclared float32 columns too; docstrings.src/arcadedb/_native/codec.py:encode_vertex_argswrites the rows without the vector columns with onejson.dumps(no hook and no_prepwhen the other values are plain scalars, decided by a census in C:_plain_rows), puts the matrices at the front of the side buffer and describes them by one"vecs": {"names", "dims", "offs"}entry. The buffer is a ctypes view of the matrix, not a copy (bytes when the array is read-only).src/arcadedb/_native/extras.py:NativeGraphBatch._create_chunkuses it; the per-row[r or {} for r in chunk]only runs when a row isNone.native/src/main/java/org/humemai/nativelib/NativeExtras.java:withVectorsturns the"vecs"entry into onefloat[]per row (bounds-checked against the side buffer) beforeGraphBatch.createVertices. The["B", offset, count]tag stays for edge properties and for a float32 column that does not qualify; such a tag then follows the matrices in the buffer. The Java is not compiled on my laptop; the native CI job builds it.docs/modules/ROOT/pages/how-to/vectors.adoc: the two paragraphs on create_vertices vectors.Stored values are the same float32 values on every path: the matrix carries the float32 bits and
Matrix.row(i)copies them. A library that predates the"vecs"entry cannot meet this Python (the library and the Python ship in one wheel);lib.GB_BINfalse keeps the text path.Tests (step 3)
CI is my only run environment (a repo hook blocks pytest and java locally), so none of the tests below ran on my laptop. Locally I only ran the pure functions (
take_vector_columns,_without_keys,_plain_rows,encode_vertex_args, and a ctypesc_chararray as ac_char_pargument) in a plain script, and the lint gates:compile(),pyflakes(its five warnings inextras.pyare on the base too), black 26.10.1, isort 9.0.2,bandit==1.9.4(clean).tests/test_vertex_vector_loads.py(both wheels,to_java_float_arrayis the shared call): stored bits for Javafloat[](JVM) or float32 ndarray (native), numpy float32, declared and not, 64 and 96 dimensions, with -0.0, the largest, the smallest normal and the smallest subnormal; the matrix against the per-vertex path; numpy float64 and lists (documented values); ragged,None, missing and aNonerow; a float64 row among float32 rows; other properties and different key orders; two vector properties and a date-time in one call; a load of chunk + 3 rows (the JVM chunk is 100,000, the native chunk iscommit_every);insert_many; and the split itself without a database.tests/test_graph_batch_vector_crossings_jvm.py(JVM only): wrapsVertexBatcher,FloatVectors,convert_python_to_javaand_to_java_property_matrix. For 250 rows in three chunks of 100 it asserts no fallback, no per-value conversion,["createVerticesJson"] * 3and oneMatrixper property and chunk, for a Javafloat[]and a float32 array, declared and not; a ragged row sends only its chunk to the fallback; a Javadouble[]is a vector only when declared;insert_manydoes no per-row insert.tests/test_native_vertex_vector_crossings.py(native only): spies onlib.calland oncodec._gb_default,codec._defaultandcodec._prep: threearcadedb_n_gb_bincrossings for three chunks, none of the three hooks ever called, no tag and no number in the text, the buffer is a ctypes view, the"vecs"offsets for two columns, a non-qualifying float32 array following the matrices, the text entry for float64 and lists and forGB_BINfalse, and a"vecs"entry outside the buffer refused.tests/test_native_graph_batch_binary_vectors.py: the one assertion that expected a"B"tag per vertex row now expects"vecs".tests/conftest.py: the two spy files are registered (test_graph_batch_vector_crossings_jvm.pyinNATIVE_NOT_APPLICABLE,test_native_vertex_vector_crossings.pyin the native-only list).What each one proves fails on the base (stated from reading the code; the run of these files on the base of #330 is linked under CI below and I have not read its result when writing this): every JVM spy case that holds a Java
float[]ordouble[](the matrix cases, the ragged case, the two-property case,insert_many) fails onfallback == []or on the per-row insert count, the float32 case on a property that is not declared fails the same way, and the same case on a declared property passes on the base (it is the control); the native spy test fails on the hook counts (the base calls_gb_defaultonce per row); the split tests fail becausetake_vector_columnsdoes not exist. The value tests pin equality with the old path and are expected to pass on both.Profile commands for the post1 A/B (step 5, nothing timed here)
Untimed phase and memory profile of the ingest shapes, cells
e2andl3d, variantcv, one fresh process per cell, 3 repetitions, once per arm (the26.11.1.post1wheel with this change against the one without it), both wheels:The figure to read is
reston the JVM wheel (was 59.2 and 60.6 percent) andprepon the native wheel (was 31.3 and 29.0 percent), plusanon_MiBandB/rowper cell.CI (the humemai fork, because the upstream runner queue is backed up)
test-native.ymlwithversion=26.11.1.post1: https://github.com/humemai/arcadedb-python/actions/runs/42616214876test-jvm.ymlwitharcadedb-tag=26.11.1: https://github.com/humemai/arcadedb-python/actions/runs/42616219493test-python-examples.ymlwitharcadedb-tag=26.11.1: https://github.com/humemai/arcadedb-python/actions/runs/42616224280perf/337-tests-on-base, JVM only; the red cases there are the proof that they fail on the base): https://github.com/humemai/arcadedb-python/actions/runs/42616346182🤖 Generated with Claude Code