Repository navigation
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
Change map
Requirement: a |
5b51082 to
1d3acd6
Compare
cab9cb7 to
91d494f
Compare
044ad8c to
b322307
Compare
d2153fe to
db52381
Compare
b322307 to
d1d6d05
Compare
d1d6d05 to
3f3d201
Compare
db52381 to
db74c6c
Compare
3f3d201 to
98fb53e
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
db74c6c to
fc4d709
Compare
98fb53e to
e2d71ad
Compare
Series: kumo-engine MLOps series: every snapshot, training table, model and prediction run carries a self-contained _provenance.json;
with kumo.mlops(...)stores each artifact under one artifact root, tracks fits and prediction runs in MLflow or Snowflake and registers models; volumes and stages can hold files; prediction runs are monitored for drift and realized performance.Stack: #53 → #112 → #50 → #60 → #64; #114 on #112; #115 on #64
Merge order: one linear stack; merge bottom to top (#53 first); #114 sits on #112 and merges any time after it; #115 (draft) sits on #64 and merges after it
This PR: Adds Unity Catalog volumes as a file destination of
DatabricksConnector.Summary
This PR adds Unity Catalog volumes as a file destination of
DatabricksConnector. Model bundles and prediction and training-table exports can live on a volume when the job runs off Databricks: a laptop, an EC2 box, SageMaker, CI. Once #50 is in, so can snapshot exports and the MLOps artifact root, with no change here. It needs only theFileStoreSinkprotocol and chunked filesystem from #112, and sits directly on it.Problem. Teams whose data and models live in Unity Catalog ask:
Today none of this works off-cluster. A
/Volumes/...path is only reachable through the FUSE mount on Databricks compute, and the Databricks sink holds no files, so every file export to it raises.Cause.
DatabricksSinkBackendwrites tables only (COPY INTO through a staging volume). It does not implementFileStoreSink, sois_file_destination(databricks, "/Volumes/...")is False, and the engine treats the path as a table name.Change.
kumo_databricks.volume.VolumeHandleris achunked_fs.ChunkedFileHandler(Let any sink hold files: FileStoreSink protocol and a chunked filesystem #112) overWorkspaceClient.files, built from the connector's own identity. Calls used:files.upload(path, stream, overwrite=True)fed from a background thread (no local disk for sized writes)files.get_metadata(HEAD; a directory reads as not found)files.list_directory_contents, recursedGET /api/2.0/fs/files<path>withRange: bytes=a-(one open-ended download serves consecutive reads); theContent-Rangestart is checked, so a server that ignores the range raises instead of returning wrong bytesfiles.delete; a tree reusesstaging.clear_volumeThe Files API caps a single upload at 5 GiB, so
part_bytesis 4 GiB. A file whose size is known when it is written (bundle files, exported Parquet parts and, with Record provenance for every artifact under one MLOps artifact root #50, snapshot index files, viaopen_sized) is one plain object up to 4 GiB, so Spark and other tools read it directly. A larger file streams into parts. A write without a known size (write_tableof a frame) stays plain up to 256 MiB, spooled under the engine scratch directory, and streams into parts above that. Errors are mapped toFileNotFoundError/PermissionError/OSErrorwith no chained cause. Any non-SDK error, such as a transport error from a presigned upload, is named by type only, so a signed URL never appears in a message.Layout on the volume, captured with Record provenance for every artifact under one MLOps artifact root #50 on top (snapshot exports and
_provenance.jsoncome from it). In Record provenance for every artifact under one MLOps artifact root #50's current layout each snapshot export sits one level deeper, in a generation directory next tokumo_snapshot.json; this PR's code is the same either way. The part size was lowered to 1 MiB for the test, so one index file splits; hashes are shortened:With the default 4 GiB part size, every file in that run is a plain object. Under an MLOps root (Record provenance for every artifact under one MLOps artifact root #50) the series layout applies (
models/<bundle id>/,predictions/<id>/predictions/).Sink routing.
holds_files(connector, path)is True only for/Volumes/<catalog>/<schema>/<volume>/...; everything else is still a table name and keeps today's code. For volume paths,write_table,write_files,publish_model(path=)andfetch_model(path=)go through Let any sink hold files: FileStoreSink protocol and a chunked filesystem #112'sFilesystemSink. Write options the sink does not use itself (such as the manifestsidecarsthat Record provenance for every artifact under one MLOps artifact root #50's training-table export and Record prediction runs and monitor score drift #64's prediction export pass; the table path does not use them) are forwarded to it unchanged. No second copy of the bundle or export logic is added.Usage (documented in the kumo-databricks README, which points to
docs/mlops.mdfor MLOps roots):On Databricks compute, a plain
/Volumes/...string still works as before, through FUSE.Client. A
WorkspaceClientis built perfilesystem()call, as the existing session code does for credential rotation. The engine holds one filesystem per transfer phase (export, load, publish), so it is one client per phase, not per file.Grants:
USE CATALOG,USE SCHEMA,READ VOLUME(load, fetch),WRITE VOLUME(export, publish). No SQL warehouse is used.Destination check before scoring. The sink's
preflight_write_files, whichTrainer.predictcalls before any scorer starts, refuses an occupied volume directory unlesswrite_mode=WriteMode.OVERWRITE; table names are not checked. With #64's default of one directory per run (<path>/<prediction id>/), a daily job's second run writes to a new directory and passes.Back-compat
Record and expose: nothing that works today changes behaviour.
DatabricksConnectorlocation=)/Volumes/...path on aDatabricksConnector/Volumes/...string root on Databricks computeFileConnectorover FUSE..chunks/parts..chunksfile is unknown to it.OSErrorsubclasses with no chained vendor text; snapshot transfers retry as for object stores.dbfs:/...,../, fewer than 3 components)Known limitations
write_tableof an in-memory frame) above 256 MiB becomes.chunks/parts, which Spark can't read directly.withstill commits what was written. This matches pyarrow's S3 behaviour; the series guards readers with_SUCCESS, provenance-last and per-range CRCs.Testing Done
New
packages/kumo-databricks/tests/test_volume.py(fake Files API intests/fakes.py):OSErrorwith no signed URL and no cause, and does not hang;Rangeis refused;PermissionError;holds_files/filesystemaccept only volume paths;tests/public/Dockerfile.dockerignoreadmitschunked_fs.py,object_store_sink.pyandartifact_export.py, so the public connector image can import the volume sink.On Let any sink hold files: FileStoreSink protocol and a chunked filesystem #112 alone: the kumo-databricks suite passes (210 passed, 1 skipped) and kumo-engine
test_filestore_sink.pypasses (4). flake8 and isort are clean.On Record prediction runs and monitor score drift #64 with this PR and Hold MLOps artifacts in Snowflake stages and score registry versions with run_batch #115 applied: the merge is clean, and the volume tests also pass with the
_provenance.jsonsidecar passed throughwrite_files.Live, on an earlier head of this PR that sat on the full series (same volume code), on a test workspace with synthetic users/items/orders data, from an EC2 box (off-cluster) and in a one-time cluster job (single-node m5d.xlarge, DBR 16.4), with wheels built from this head:
_SUCCESS, full CRC matchSnapshotOutputConfig(part size lowered to 1 MiB for the test socol_stats.jsonsplits; read across its boundary exact)_provenance.jsonrecording the volume pathkumo.mlops(kumo.MLOps(artifact_root=(databricks, "/Volumes/...")))fit + predict:snapshots/,training_tables/,models/,predictions/under the root, 4_provenance.jsonRead throughput on 2 GiB of one part (MB/s), handler vs raw Files API:
Checklist
🤖 Generated with Claude Code