blob: 26a237be491e48c1b590ad876a7e5fb6f0528a58 [file]
// Databricks notebook source
//-------------------------------------------------------------
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
//-------------------------------------------------------------
// SystemDS on Databricks: Unity Catalog round-trip
//
// Reads a table from Unity Catalog into SystemDS, runs a DML script over it,
// and writes the result back as a UC table. Prereq: `SystemDS.jar` installed
// on the cluster (DBR 16.4 LTS, Spark 3.5.2 / Scala 2.12) with the SystemDS
// JVM flags.
//
// Everything is configured via the notebook widgets (first cell) so this works
// in any workspace:
// - catalog / schema / input_table / output_table: where data is read/written
// - dml_path: path to a DML script to run (blank = built-in z-score demo)
// - exec_type: SystemDS execution mode (DEFAULT = let SystemDS decide)
//
// The DML script contract: it receives the input matrix as `X` and must
// produce a matrix `Y` and a scalar `checksum`.
// COMMAND ----------
// Widgets make the notebook portable: set these per workspace instead of
// hardcoding values. The table location defaults to main.default.
dbutils.widgets.text("catalog", "main", "Unity Catalog")
dbutils.widgets.text("schema", "default", "Schema")
dbutils.widgets.text("input_table", "systemds_input", "Input table name")
dbutils.widgets.text("output_table", "systemds_output", "Output table name")
// DML script to run. Blank uses the built-in z-score demo below. Otherwise a
// path readable from the cluster driver, e.g. a UC volume, /Workspace, or dbfs:
// /Volumes/<catalog>/<schema>/<volume>/my_script.dml
dbutils.widgets.text("dml_path", "", "DML script path (blank = built-in demo)")
// SystemDS execution mode. DEFAULT lets SystemDS choose (no forcing).
dbutils.widgets.dropdown("exec_type", "DEFAULT",
Seq("DEFAULT", "DRIVER", "SPARK", "DRIVER_AND_SPARK"), "Execution type")
val catalog = dbutils.widgets.get("catalog")
val schema = dbutils.widgets.get("schema")
val INPUT_TABLE = s"$catalog.$schema.${dbutils.widgets.get("input_table")}"
val OUTPUT_TABLE = s"$catalog.$schema.${dbutils.widgets.get("output_table")}"
val DML_PATH = dbutils.widgets.get("dml_path")
val EXEC_TYPE = dbutils.widgets.get("exec_type")
// COMMAND ----------
import org.apache.sysds.api.mlcontext._
import org.apache.sysds.api.mlcontext.ScriptFactory._
import org.apache.sysds.utils.Statistics
import org.apache.spark.sql.functions._
val ml = new MLContext(sc)
ml.setStatistics(true)
// Only override the execution mode when explicitly requested; DEFAULT leaves
// SystemDS to pick the plan (it will use Spark only when it decides to).
if (EXEC_TYPE != "DEFAULT")
ml.setExecutionType(MLContext.ExecutionType.valueOf(EXEC_TYPE))
// SystemDS defaults to a relative scratch dir, which the Databricks default
// filesystem rejects ("Path must be absolute"). Pin both to absolute paths.
ml.setConfigProperty("sysds.scratch", "/tmp/systemds_scratch")
ml.setConfigProperty("sysds.localtmpdir", "/local_disk0/tmp/systemds")
println("Spark version: " + sc.version + " | exec_type: " + EXEC_TYPE)
// COMMAND ----------
// MAGIC %md
// MAGIC ## Setup: ensure an input table exists in the catalog
// COMMAND ----------
if (!spark.catalog.tableExists(INPUT_TABLE)) {
val seed = spark.range(0, 5000).select(
(rand(1) * 100).as("f1"),
(rand(2) * 10 + 5).as("f2"),
(rand(3) - 0.5).as("f3"),
(rand(4) * 1000).as("f4"))
seed.write.mode("overwrite").saveAsTable(INPUT_TABLE)
}
println(s"input table $INPUT_TABLE rows = ${spark.table(INPUT_TABLE).count()}")
// COMMAND ----------
// MAGIC %md
// MAGIC ## 1. Read a table from the catalog into SystemDS
// COMMAND ----------
val inDF = spark.table(INPUT_TABLE)
.select(col("f1").cast("double"), col("f2").cast("double"),
col("f3").cast("double"), col("f4").cast("double"))
// Built-in fallback: standardize the columns (z-score). Used when no dml_path
// widget is set. A custom script must read `X` and produce `Y` and `checksum`.
val defaultScript = """
n = nrow(X)
mu = colMeans(X)
Xc = X - mu
variance = colSums(Xc^2) / (n - 1)
sigma = sqrt(variance)
Y = Xc / sigma
checksum = sum(Y)
print("standardized " + nrow(X) + " x " + ncol(X) + " matrix")
"""
val baseScript = if (DML_PATH.nonEmpty) {
println(s"running DML from $DML_PATH")
dmlFromFile(DML_PATH)
} else {
println("running built-in z-score demo script")
dml(defaultScript)
}
val script = baseScript.in("X", inDF).out("Y", "checksum")
val res = ml.execute(script)
val checksum = res.getDouble("checksum")
val outDF = res.getDataFrameDoubleNoIDColumn("Y")
println(s"checksum(Y) = $checksum (≈0 for standardized data)")
// COMMAND ----------
// MAGIC %md
// MAGIC ## 2. Write the SystemDS result back to the catalog
// COMMAND ----------
outDF.write.mode("overwrite").saveAsTable(OUTPUT_TABLE)
val outRows = spark.table(OUTPUT_TABLE).count()
val outCols = spark.table(OUTPUT_TABLE).columns.length
println(s"wrote $OUTPUT_TABLE: $outRows rows x $outCols cols")
// COMMAND ----------
val spExecuted = Statistics.getNoOfExecutedSPInst()
dbutils.notebook.exit(
s"spark=${sc.version} in=$INPUT_TABLE out=$OUTPUT_TABLE " +
s"out_rows=$outRows out_cols=$outCols checksum=$checksum sp_executed=$spExecuted")