From e090b2e8700338b436a3b60b6aa801293aa375f8 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 14 Sep 2025 20:50:50 +0200 Subject: [PATCH 01/16] Add interfaces for tensors, tensors data and backend --- .github/worklfows/publish.yml | 26 +++++++ build.gradle.kts | 12 +--- gradle.properties | 6 ++ gradle/libs.versions.toml | 29 +------- settings.gradle.kts | 2 + .../skainet-tensors-api/gradle.properties | 2 + .../kotlin/sk/ai/net/core/tensor/DType.kt | 33 +++++++++ .../kotlin/sk/ai/net/core/tensor/Shape.kt | 62 ++++++++++++++++ .../kotlin/sk/ai/net/core/tensor/Tensor.kt | 10 +++ .../sk/ai/net/core/tensor/TensorData.kt | 19 +++++ .../kotlin/sk/ai/net/core/tensor/TensorOps.kt | 71 +++++++++++++++++++ 11 files changed, 236 insertions(+), 36 deletions(-) create mode 100644 .github/worklfows/publish.yml create mode 100644 skainet-core/skainet-tensors-api/gradle.properties create mode 100644 skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/DType.kt create mode 100644 skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Shape.kt create mode 100644 skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Tensor.kt create mode 100644 skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorData.kt create mode 100644 skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorOps.kt diff --git a/.github/worklfows/publish.yml b/.github/worklfows/publish.yml new file mode 100644 index 000000000..8731476f5 --- /dev/null +++ b/.github/worklfows/publish.yml @@ -0,0 +1,26 @@ +name: release + +on: + push: + tags: + - '**' + +jobs: + publish: + name: Release build and publish + runs-on: macOS-latest + steps: + - name: Check out code + uses: actions/checkout@v5 + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: 'zulu' + java-version: 21 + - name: Publish to MavenCentral + run: ./gradlew publish --no-configuration-cache --stacktrace + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 534ff9a38..b007dc946 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,18 +2,10 @@ plugins { alias(libs.plugins.androidLibrary) apply false alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.jetbrainsKotlinJvm) apply false - alias(libs.plugins.binaryCompatibility) apply false - alias(libs.plugins.modulegraph.souza) apply true - - + alias(libs.plugins.vanniktech.mavenPublish) apply false } allprojects { group = "sk.ai.net" version = "0.0.7" -} - -moduleGraphConfig { - readmePath.set("./Modules.md") - heading = "### Module Graph" -} +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 987d6bc1d..ed1cf6139 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,9 @@ +GROUP=sk.ainet.core +VERSION_NAME=0.0.8 + +mavenCentralPublishing=true +mavenCentralAutomaticPublishing=true + #Gradle org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" org.gradle.caching=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4db4bb0e..67b9182e8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,39 +1,16 @@ [versions] -agp = "8.12.1" -kotlin = "2.2.10" +agp = "8.11.2" +kotlin = "2.2.20" kotlinx-coroutines = "1.10.2" android-minSdk = "24" -android-compileSdk = "35" -kotlinxSerializationJson = "1.9.0" -ktorClientCore = "3.2.3" -ktorClientPlugins = "3.1.1" -logbackClassic = "1.5.18" -kotlinxIo = "0.8.0" -kotlinxCli = "0.3.6" -nexus-publish = "2.0.0" -testng = "7.10.2" -binaryCompatibility = "0.18.1" -moduleGraphSouza = "0.12.1" +android-compileSdk = "36" [libraries] kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -kotlinx-coroutines-core-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "kotlinxSerializationJson" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } -ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientCore" } -ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorClientCore" } -ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCore" } -ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktorClientCore" } -ktor-client-plugins = { module = "io.ktor:ktor-client-plugins", version.ref = "ktorClientPlugins" } -logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logbackClassic" } -kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIo" } -kotlinx-cli = { module = "org.jetbrains.kotlinx:kotlinx-cli", version.ref = "kotlinxCli" } [plugins] androidLibrary = { id = "com.android.library", version.ref = "agp" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -binaryCompatibility = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibility" } -modulegraph-souza = { id = "dev.iurysouza.modulegraph", version.ref = "moduleGraphSouza" } vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.34.0" } diff --git a/settings.gradle.kts b/settings.gradle.kts index a3b97062e..8dda2296f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,3 +14,5 @@ dependencyResolutionManagement { } rootProject.name = "skainet" + +include("skainet-core:skainet-tensors-api") diff --git a/skainet-core/skainet-tensors-api/gradle.properties b/skainet-core/skainet-tensors-api/gradle.properties new file mode 100644 index 000000000..9af8a1d77 --- /dev/null +++ b/skainet-core/skainet-tensors-api/gradle.properties @@ -0,0 +1,2 @@ +POM_ARTIFACT_ID=core-api +POM_NAME=skainet core API \ No newline at end of file diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/DType.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/DType.kt new file mode 100644 index 000000000..96346668c --- /dev/null +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/DType.kt @@ -0,0 +1,33 @@ +package sk.ai.net.core.tensor + +// Base marker interface for all dtypes +public sealed interface DType { + public val sizeInBits: Int + public val name: String +} + +public object Ternary : DType { + override val sizeInBits: Int = 2 + override val name: String = "Ternary" +} + + +public object Int4 : DType { + override val sizeInBits: Int = 4 // technically 4 bits, but minimum is 1 byte + override val name: String = "Int4" +} + +public object Int8 : DType { + override val sizeInBits: Int = 8 // technically 4 bits, but minimum is 1 byte + override val name: String = "Int8" +} + +public object FP16 : DType { + override val sizeInBits: Int = 16 + override val name: String = "Float16" +} + +public object FP32 : DType{ + override val sizeInBits: Int = 32 + override val name: String = "Float32" +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Shape.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Shape.kt new file mode 100644 index 000000000..538451937 --- /dev/null +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Shape.kt @@ -0,0 +1,62 @@ +package sk.ai.net.core.tensor + +/** + * Data class representing the shape of a multi-dimensional array (tensor). + * + * @property dimensions An array of integers representing the size of each dimension. + */ + +public data class Shape(val dimensions: IntArray) { + public companion object { + public operator fun invoke(vararg dimensions: Int): Shape { + return Shape(dimensions.copyOf()) + } + } + + val volume: Int + get() = dimensions.fold(1) { a, x -> a * x } + + val rank: Int + get() = dimensions.size + + internal fun index(indices: IntArray): Int { + assert( + { indices.size == dimensions.size }, + { "`indices.size` must be ${dimensions.size}: ${indices.size}" }) + return dimensions.zip(indices).fold(0) { a, x -> + assert( + { 0 <= x.second && x.second < x.first }, + { "Illegal index: indices = ${indices}, shape = $dimensions" }) + a * x.first + x.second + } + } + + public operator fun get(index: Int): Int { + return dimensions[index] + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Shape) return false + + return dimensions.contentEquals(other.dimensions) + } + + override fun hashCode(): Int { + return dimensions.contentHashCode() + } + + override fun toString(): String { + // Create a string representation of the dimensions array + val dimensionsString = dimensions.joinToString(separator = " x ", prefix = "[", postfix = "]") + // Return the formatted string including dimensions and volume + return "Shape: Dimensions = $dimensionsString, Size (Volume) = $volume" + } +} + +internal inline fun assert(value: () -> Boolean, lazyMessage: () -> Any) { + if (!value()) { + val message = lazyMessage() + throw AssertionError(message) + } +} diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Tensor.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Tensor.kt new file mode 100644 index 000000000..ee7881d0f --- /dev/null +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Tensor.kt @@ -0,0 +1,10 @@ +package sk.ai.net.core.tensor + +/** + * Interface representing a multi-dimensional array of numeric values. + * + * A Tensor is a generalization of vectors and matrices to potentially higher dimensions. + * The [shape] property defines the dimensions of the tensor, and the [get] operator + * allows accessing individual elements by their indices. + */ +public interface Tensor : TensorData, TensorOps> diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorData.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorData.kt new file mode 100644 index 000000000..27a4a781b --- /dev/null +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorData.kt @@ -0,0 +1,19 @@ +package sk.ai.net.core.tensor + +public interface TensorData { + /** + * The shape of the tensor, represented as a Shape data class containing dimensions. + * + * For example, a scalar has an empty shape, a vector has a shape with one dimension, + * a matrix has a shape with two dimensions, and so on. + */ + public val shape: Shape + + /** + * Retrieves the value at the specified indices. + * + * @param indices The indices of the element to retrieve. + * @return The value at the specified indices. + */ + public operator fun get(vararg indices: Int): V +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorOps.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorOps.kt new file mode 100644 index 000000000..013068673 --- /dev/null +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorOps.kt @@ -0,0 +1,71 @@ +package sk.ai.net.core.tensor + +/** + * Interface representing mathematical operations on tensors. + * + * Keeps math separate from storage, so the same math API can work + * with multiple backends (dense, sparse, GPU, etc.). + */ +public interface TensorOps> { + /** + * Performs matrix multiplication of two tensors. + * + * @param other The tensor to multiply with. + * @return The result of matrix multiplication. + */ + public fun matmul(a: T, b: T): T + public fun scale(a: T, scalar: Double): T + public fun dot(a: T, b: T): Double + + // Tensor–Tensor + /** + * Adds two tensors element-wise. + * + * @param other The tensor to add. + * @return The result of adding the two tensors. + */ + public operator fun T.plus(other: T): T + public operator fun T.minus(other: T): T + /** + * Multiplies two tensors element-wise. + * + * @param other The tensor to multiply with. + * @return The result of multiplying the two tensors. + */ + public operator fun T.times(other: T): T // elementwise multiply + public operator fun T.div(other: T): T // elementwise divide + + // Tensor–Scalar + public operator fun T.plus(scalar: Int): T + public operator fun T.minus(scalar: Int): T + public operator fun T.times(scalar: Int): T + public operator fun T.div(scalar: Int): T + + public operator fun T.plus(scalar: Float): T + public operator fun T.minus(scalar: Float): T + public operator fun T.times(scalar: Float): T + public operator fun T.div(scalar: Float): T + + public operator fun T.plus(scalar: Double): T + public operator fun T.minus(scalar: Double): T + public operator fun T.times(scalar: Double): T + public operator fun T.div(scalar: Double): T + + + // Scalar–Tensor + public operator fun Double.plus(t: T): T + public operator fun Double.minus(t: T): T + public operator fun Double.times(t: T): T + public operator fun Double.div(t: T): T + + // Optionally Int & Float (delegate to Double) + public operator fun Int.plus(t: T): T = this.toDouble() + t + public operator fun Int.minus(t: T): T = this.toDouble() - t + public operator fun Int.times(t: T): T = this.toDouble() * t + public operator fun Int.div(t: T): T = this.toDouble() / t + + public operator fun Float.plus(t: T): T = this.toDouble() + t + public operator fun Float.minus(t: T): T = this.toDouble() - t + public operator fun Float.times(t: T): T = this.toDouble() * t + public operator fun Float.div(t: T): T = this.toDouble() / t +} \ No newline at end of file From 91df5d5f6a467f7a0e9e87abe0cbef5f93f2a3a1 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 14 Sep 2025 20:51:35 +0200 Subject: [PATCH 02/16] Add gradle build file --- .../skainet-tensors-api/build.gradle.kts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 skainet-core/skainet-tensors-api/build.gradle.kts diff --git a/skainet-core/skainet-tensors-api/build.gradle.kts b/skainet-core/skainet-tensors-api/build.gradle.kts new file mode 100644 index 000000000..c4f3ef990 --- /dev/null +++ b/skainet-core/skainet-tensors-api/build.gradle.kts @@ -0,0 +1,53 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.vanniktech.mavenPublish) +} + +kotlin { + explicitApi() + + androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + iosArm64() + iosSimulatorArm64() + macosArm64 () + linuxX64 () + linuxArm64 () + + jvm() + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } +} + +android { + namespace = "sk.ai.net.core.api" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} \ No newline at end of file From cea964a3bda5f9399d6e88b82fc5b0f80bd2d151 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Sun, 14 Sep 2025 23:25:53 +0200 Subject: [PATCH 03/16] Add initial naive implementation of tensors backend for CPU with Float --- settings.gradle.kts | 1 + .../skainet-tensors-api/gradle.properties | 2 +- .../sk/ai/net/core/tensor/ComputeBackend.kt | 21 + .../kotlin/sk/ai/net/core/tensor/Shape.kt | 2 +- skainet-core/skainet-tensors/build.gradle.kts | 59 +++ .../skainet-tensors/gradle.properties | 2 + .../ai/net/core/tensor/backend/CpuBackend.kt | 285 +++++++++++++ .../kotlin/sk/ai/net/core/tensor/TestUtils.kt | 45 +++ .../net/core/tensor/backend/CpuBackendTest.kt | 369 +++++++++++++++++ .../tensor/multiplication/EdgeCasesTest.kt | 320 +++++++++++++++ .../ElementwiseMultiplicationTest.kt | 192 +++++++++ .../tensor/multiplication/MLOperationsTest.kt | 379 ++++++++++++++++++ .../MatrixMultiplicationTest.kt | 237 +++++++++++ 13 files changed, 1912 insertions(+), 2 deletions(-) create mode 100644 skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/ComputeBackend.kt create mode 100644 skainet-core/skainet-tensors/build.gradle.kts create mode 100644 skainet-core/skainet-tensors/gradle.properties create mode 100644 skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ai/net/core/tensor/backend/CpuBackend.kt create mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/TestUtils.kt create mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/backend/CpuBackendTest.kt create mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/EdgeCasesTest.kt create mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/ElementwiseMultiplicationTest.kt create mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MLOperationsTest.kt create mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MatrixMultiplicationTest.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 8dda2296f..3c9efde5a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,3 +16,4 @@ dependencyResolutionManagement { rootProject.name = "skainet" include("skainet-core:skainet-tensors-api") +include("skainet-core:skainet-tensors") diff --git a/skainet-core/skainet-tensors-api/gradle.properties b/skainet-core/skainet-tensors-api/gradle.properties index 9af8a1d77..cc0be2341 100644 --- a/skainet-core/skainet-tensors-api/gradle.properties +++ b/skainet-core/skainet-tensors-api/gradle.properties @@ -1,2 +1,2 @@ -POM_ARTIFACT_ID=core-api +POM_ARTIFACT_ID=tensors-api POM_NAME=skainet core API \ No newline at end of file diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/ComputeBackend.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/ComputeBackend.kt new file mode 100644 index 000000000..02ca02e8d --- /dev/null +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/ComputeBackend.kt @@ -0,0 +1,21 @@ +package sk.ai.net.core.tensor.backend + +import sk.ai.net.core.tensor.DType +import sk.ai.net.core.tensor.Tensor +import sk.ai.net.core.tensor.TensorOps + +/** + * Interface representing a computation backend for tensor operations. + * + * A computation backend is responsible for executing tensor operations on a specific + * hardware platform (CPU, GPU, etc.). Different backends can provide different + * implementations of the same operations, optimized for their target platform. + */ +public interface ComputeBackend : TensorOps> { + /** + * The name of the backend. + */ + public val name: String + + +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Shape.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Shape.kt index 538451937..2e50cd52b 100644 --- a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Shape.kt +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Shape.kt @@ -19,7 +19,7 @@ public data class Shape(val dimensions: IntArray) { val rank: Int get() = dimensions.size - internal fun index(indices: IntArray): Int { + public fun index(indices: IntArray): Int { assert( { indices.size == dimensions.size }, { "`indices.size` must be ${dimensions.size}: ${indices.size}" }) diff --git a/skainet-core/skainet-tensors/build.gradle.kts b/skainet-core/skainet-tensors/build.gradle.kts new file mode 100644 index 000000000..79eb06b1e --- /dev/null +++ b/skainet-core/skainet-tensors/build.gradle.kts @@ -0,0 +1,59 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.vanniktech.mavenPublish) +} + +kotlin { + explicitApi() + + androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + iosArm64() + iosSimulatorArm64() + macosArm64 () + linuxX64 () + linuxArm64 () + + jvm() + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":skainet-core:skainet-tensors-api")) + } + } + + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } +} + +android { + namespace = "sk.ai.net.core.api" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors/gradle.properties b/skainet-core/skainet-tensors/gradle.properties new file mode 100644 index 000000000..a6f6cff10 --- /dev/null +++ b/skainet-core/skainet-tensors/gradle.properties @@ -0,0 +1,2 @@ +POM_ARTIFACT_ID=tensors +POM_NAME=skainet core API \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ai/net/core/tensor/backend/CpuBackend.kt b/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ai/net/core/tensor/backend/CpuBackend.kt new file mode 100644 index 000000000..067e80c3d --- /dev/null +++ b/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ai/net/core/tensor/backend/CpuBackend.kt @@ -0,0 +1,285 @@ +package sk.ai.net.core.tensor.backend + +import sk.ai.net.core.tensor.* + +/** + * A CPU-based tensor for FP32/Float values. + * + * This tensor stores data on the CPU using simple FloatArray with NCHW row-major layout. + * It supports 1-4 dimensional tensors and delegates all operations to CpuBackend. + */ +public class CpuTensorFP32( + override val shape: Shape, + internal val data: FloatArray +) : Tensor { + + init { + require(data.size == shape.volume) { + "Data size ${data.size} doesn't match shape volume ${shape.volume}" + } + require(shape.rank in 1..4) { + "Only 1-4 dimensional tensors are supported, got ${shape.rank}" + } + } + + override fun get(vararg indices: Int): Float { + val index = shape.index(indices) + return data[index] + } + + // Delegate all operations to the CpuBackend + private val backend = CpuBackend() + + override fun matmul(a: Tensor, b: Tensor): Tensor = backend.matmul(a, b) + override fun scale(a: Tensor, scalar: Double): Tensor = backend.scale(a, scalar) + override fun dot(a: Tensor, b: Tensor): Double = backend.dot(a, b) + + // Tensor-Tensor operations + override fun Tensor.plus(other: Tensor): Tensor = with(backend) { this@plus.plus(other) } + override fun Tensor.minus(other: Tensor): Tensor = with(backend) { this@minus.minus(other) } + override fun Tensor.times(other: Tensor): Tensor = with(backend) { this@times.times(other) } + override fun Tensor.div(other: Tensor): Tensor = with(backend) { this@div.div(other) } + + // Tensor-Scalar operations - delegate to backend + override fun Tensor.plus(scalar: Int): Tensor = with(backend) { this@plus.plus(scalar) } + override fun Tensor.minus(scalar: Int): Tensor = with(backend) { this@minus.minus(scalar) } + override fun Tensor.times(scalar: Int): Tensor = with(backend) { this@times.times(scalar) } + override fun Tensor.div(scalar: Int): Tensor = with(backend) { this@div.div(scalar) } + + override fun Tensor.plus(scalar: Float): Tensor = with(backend) { this@plus.plus(scalar) } + override fun Tensor.minus(scalar: Float): Tensor = with(backend) { this@minus.minus(scalar) } + override fun Tensor.times(scalar: Float): Tensor = with(backend) { this@times.times(scalar) } + override fun Tensor.div(scalar: Float): Tensor = with(backend) { this@div.div(scalar) } + + override fun Tensor.plus(scalar: Double): Tensor = with(backend) { this@plus.plus(scalar) } + override fun Tensor.minus(scalar: Double): Tensor = with(backend) { this@minus.minus(scalar) } + override fun Tensor.times(scalar: Double): Tensor = with(backend) { this@times.times(scalar) } + override fun Tensor.div(scalar: Double): Tensor = with(backend) { this@div.div(scalar) } + + // Scalar-Tensor operations - delegate to backend + override fun Double.plus(t: Tensor): Tensor = with(backend) { this@plus.plus(t) } + override fun Double.minus(t: Tensor): Tensor = with(backend) { this@minus.minus(t) } + override fun Double.times(t: Tensor): Tensor = with(backend) { this@times.times(t) } + override fun Double.div(t: Tensor): Tensor = with(backend) { this@div.div(t) } + + public companion object { + /** + * Creates a tensor from an array with the given shape. + */ + public fun fromArray(shape: Shape, data: FloatArray): CpuTensorFP32 { + return CpuTensorFP32(shape, data) + } + + /** + * Creates a tensor filled with zeros. + */ + public fun zeros(shape: Shape): CpuTensorFP32 { + return CpuTensorFP32(shape, FloatArray(shape.volume)) + } + + /** + * Creates a tensor filled with ones. + */ + public fun ones(shape: Shape): CpuTensorFP32 { + return CpuTensorFP32(shape, FloatArray(shape.volume) { 1.0f }) + } + + /** + * Creates a tensor filled with a specific value. + */ + public fun full(shape: Shape, value: Float): CpuTensorFP32 { + return CpuTensorFP32(shape, FloatArray(shape.volume) { value }) + } + } +} + +/** + * A CPU-based implementation of the ComputeBackend interface for FP32/Float tensors. + */ +public class CpuBackend : ComputeBackend { + override val name: String = "CPU" + + // Basic operations - implement the actual computation logic + override fun matmul(a: Tensor, b: Tensor): Tensor { + require(a is CpuTensorFP32 && b is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } + require(a.shape.rank == 2 && b.shape.rank == 2) { "Matrix multiplication requires 2D tensors" } + require(a.shape[1] == b.shape[0]) { "Matrix dimensions don't match for multiplication" } + + val rows = a.shape[0] + val cols = b.shape[1] + val inner = a.shape[1] + val result = FloatArray(rows * cols) + + for (i in 0 until rows) { + for (j in 0 until cols) { + var sum = 0f + for (k in 0 until inner) { + sum += a.data[i * inner + k] * b.data[k * cols + j] + } + result[i * cols + j] = sum + } + } + + return CpuTensorFP32(Shape(rows, cols), result) + } + + override fun scale(a: Tensor, scalar: Double): Tensor { + require(a is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = a.data.map { it * scalar.toFloat() }.toFloatArray() + return CpuTensorFP32(a.shape, result) + } + + override fun dot(a: Tensor, b: Tensor): Double { + require(a is CpuTensorFP32 && b is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } + require(a.shape == b.shape) { "Tensors must have same shape for dot product" } + + var sum = 0.0 + for (i in a.data.indices) { + sum += a.data[i] * b.data[i] + } + return sum + } + + // Tensor-Tensor operations - implement actual computation logic + override fun Tensor.plus(other: Tensor): Tensor { + require(this is CpuTensorFP32 && other is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } + require(this.shape == other.shape) { "Tensors must have same shape for addition" } + + val result = FloatArray(this.data.size) + for (i in this.data.indices) { + result[i] = this.data[i] + other.data[i] + } + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.minus(other: Tensor): Tensor { + require(this is CpuTensorFP32 && other is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } + require(this.shape == other.shape) { "Tensors must have same shape for subtraction" } + + val result = FloatArray(this.data.size) + for (i in this.data.indices) { + result[i] = this.data[i] - other.data[i] + } + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.times(other: Tensor): Tensor { + require(this is CpuTensorFP32 && other is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } + require(this.shape == other.shape) { "Tensors must have same shape for element-wise multiplication" } + + val result = FloatArray(this.data.size) + for (i in this.data.indices) { + result[i] = this.data[i] * other.data[i] + } + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.div(other: Tensor): Tensor { + require(this is CpuTensorFP32 && other is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } + require(this.shape == other.shape) { "Tensors must have same shape for element-wise division" } + + val result = FloatArray(this.data.size) + for (i in this.data.indices) { + result[i] = this.data[i] / other.data[i] + } + return CpuTensorFP32(this.shape, result) + } + + // Tensor-Scalar operations + override fun Tensor.plus(scalar: Int): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it + scalar }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.minus(scalar: Int): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it - scalar }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.times(scalar: Int): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it * scalar }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.div(scalar: Int): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it / scalar }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.plus(scalar: Float): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it + scalar }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.minus(scalar: Float): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it - scalar }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.times(scalar: Float): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it * scalar }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.div(scalar: Float): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it / scalar }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.plus(scalar: Double): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it + scalar.toFloat() }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.minus(scalar: Double): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it - scalar.toFloat() }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.times(scalar: Double): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it * scalar.toFloat() }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + override fun Tensor.div(scalar: Double): Tensor { + require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = this.data.map { it / scalar.toFloat() }.toFloatArray() + return CpuTensorFP32(this.shape, result) + } + + // Scalar-Tensor operations + override fun Double.plus(t: Tensor): Tensor { + require(t is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = t.data.map { this.toFloat() + it }.toFloatArray() + return CpuTensorFP32(t.shape, result) + } + + override fun Double.minus(t: Tensor): Tensor { + require(t is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = t.data.map { this.toFloat() - it }.toFloatArray() + return CpuTensorFP32(t.shape, result) + } + + override fun Double.times(t: Tensor): Tensor { + require(t is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = t.data.map { this.toFloat() * it }.toFloatArray() + return CpuTensorFP32(t.shape, result) + } + + override fun Double.div(t: Tensor): Tensor { + require(t is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } + val result = t.data.map { this.toFloat() / it }.toFloatArray() + return CpuTensorFP32(t.shape, result) + } +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/TestUtils.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/TestUtils.kt new file mode 100644 index 000000000..bab3d93a7 --- /dev/null +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/TestUtils.kt @@ -0,0 +1,45 @@ +package sk.ai.net.core.tensor + +import kotlin.test.assertTrue +import kotlin.math.abs + +/** + * Utility functions for testing with float precision handling + */ + +/** + * Asserts that two float values are equal within a specified tolerance/precision + */ +fun assertFloatEquals(expected: Float, actual: Float, precision: Float = 1e-6f, message: String? = null) { + val diff = abs(expected - actual) + val errorMessage = message ?: "Expected $expected but was $actual (difference: $diff, tolerance: $precision)" + assertTrue(diff <= precision, errorMessage) +} + +/** + * Asserts that two double values are equal within a specified tolerance/precision + */ +fun assertDoubleEquals(expected: Double, actual: Double, precision: Double = 1e-6, message: String? = null) { + val diff = abs(expected - actual) + val errorMessage = message ?: "Expected $expected but was $actual (difference: $diff, tolerance: $precision)" + assertTrue(diff <= precision, errorMessage) +} + +/** + * Asserts that a float value is within a relative tolerance of the expected value + * Useful for large numbers where absolute tolerance might not be appropriate + */ +fun assertFloatEqualsRelative(expected: Float, actual: Float, relativeTolerance: Float = 1e-5f, message: String? = null) { + val relativeError = abs((expected - actual) / expected) + val errorMessage = message ?: "Expected $expected but was $actual (relative error: $relativeError, tolerance: $relativeTolerance)" + assertTrue(relativeError <= relativeTolerance, errorMessage) +} + +/** + * Asserts that a double value is within a relative tolerance of the expected value + */ +fun assertDoubleEqualsRelative(expected: Double, actual: Double, relativeTolerance: Double = 1e-5, message: String? = null) { + val relativeError = abs((expected - actual) / expected) + val errorMessage = message ?: "Expected $expected but was $actual (relative error: $relativeError, tolerance: $relativeTolerance)" + assertTrue(relativeError <= relativeTolerance, errorMessage) +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/backend/CpuBackendTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/backend/CpuBackendTest.kt new file mode 100644 index 000000000..09dbc04ab --- /dev/null +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/backend/CpuBackendTest.kt @@ -0,0 +1,369 @@ +package sk.ai.net.core.tensor.backend + +import sk.ai.net.core.tensor.* +import kotlin.test.* + +class CpuBackendTest { + + private val backend = CpuBackend() + + @Test + fun testTensorCreation1D() { + val shape = Shape(5) + val data = floatArrayOf(1f, 2f, 3f, 4f, 5f) + val tensor = CpuTensorFP32.fromArray(shape, data) + + assertEquals(shape, tensor.shape) + assertEquals(5, tensor.shape.volume) + assertEquals(1, tensor.shape.rank) + + for (i in 0 until 5) { + assertEquals(data[i], tensor[i]) + } + } + + @Test + fun testTensorCreation2D() { + val shape = Shape(2, 3) + val data = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) + val tensor = CpuTensorFP32.fromArray(shape, data) + + assertEquals(shape, tensor.shape) + assertEquals(6, tensor.shape.volume) + assertEquals(2, tensor.shape.rank) + + // Test NCHW row-major layout + assertEquals(1f, tensor[0, 0]) + assertEquals(2f, tensor[0, 1]) + assertEquals(3f, tensor[0, 2]) + assertEquals(4f, tensor[1, 0]) + assertEquals(5f, tensor[1, 1]) + assertEquals(6f, tensor[1, 2]) + } + + @Test + fun testTensorCreation3D() { + val shape = Shape(2, 2, 2) + val data = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f) + val tensor = CpuTensorFP32.fromArray(shape, data) + + assertEquals(shape, tensor.shape) + assertEquals(8, tensor.shape.volume) + assertEquals(3, tensor.shape.rank) + + // Test NCHW layout for 3D tensor + assertEquals(1f, tensor[0, 0, 0]) + assertEquals(2f, tensor[0, 0, 1]) + assertEquals(3f, tensor[0, 1, 0]) + assertEquals(4f, tensor[0, 1, 1]) + assertEquals(5f, tensor[1, 0, 0]) + assertEquals(6f, tensor[1, 0, 1]) + assertEquals(7f, tensor[1, 1, 0]) + assertEquals(8f, tensor[1, 1, 1]) + } + + @Test + fun testTensorCreation4D() { + val shape = Shape(1, 2, 2, 2) // NCHW: 1 batch, 2 channels, 2x2 spatial + val data = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f) + val tensor = CpuTensorFP32.fromArray(shape, data) + + assertEquals(shape, tensor.shape) + assertEquals(8, tensor.shape.volume) + assertEquals(4, tensor.shape.rank) + + // Test NCHW layout for 4D tensor + assertEquals(1f, tensor[0, 0, 0, 0]) + assertEquals(2f, tensor[0, 0, 0, 1]) + assertEquals(3f, tensor[0, 0, 1, 0]) + assertEquals(4f, tensor[0, 0, 1, 1]) + assertEquals(5f, tensor[0, 1, 0, 0]) + assertEquals(6f, tensor[0, 1, 0, 1]) + assertEquals(7f, tensor[0, 1, 1, 0]) + assertEquals(8f, tensor[0, 1, 1, 1]) + } + + @Test + fun testZerosTensor() { + val shape = Shape(2, 3) + val tensor = CpuTensorFP32.zeros(shape) + + assertEquals(shape, tensor.shape) + for (i in 0 until 2) { + for (j in 0 until 3) { + assertEquals(0f, tensor[i, j]) + } + } + } + + @Test + fun testOnesTensor() { + val shape = Shape(2, 3) + val tensor = CpuTensorFP32.ones(shape) + + assertEquals(shape, tensor.shape) + for (i in 0 until 2) { + for (j in 0 until 3) { + assertEquals(1f, tensor[i, j]) + } + } + } + + @Test + fun testFullTensor() { + val shape = Shape(2, 3) + val value = 5.5f + val tensor = CpuTensorFP32.full(shape, value) + + assertEquals(shape, tensor.shape) + for (i in 0 until 2) { + for (j in 0 until 3) { + assertEquals(value, tensor[i, j]) + } + } + } + + @Test + fun testTensorAddition() { + val shape = Shape(2, 3) + val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) + val data2 = floatArrayOf(2f, 3f, 4f, 5f, 6f, 7f) + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 + tensor2 } + + assertEquals(shape, result.shape) + assertEquals(3f, result[0, 0]) // 1 + 2 + assertEquals(5f, result[0, 1]) // 2 + 3 + assertEquals(7f, result[0, 2]) // 3 + 4 + assertEquals(9f, result[1, 0]) // 4 + 5 + assertEquals(11f, result[1, 1]) // 5 + 6 + assertEquals(13f, result[1, 2]) // 6 + 7 + } + + @Test + fun testTensorSubtraction() { + val shape = Shape(2, 2) + val data1 = floatArrayOf(5f, 6f, 7f, 8f) + val data2 = floatArrayOf(1f, 2f, 3f, 4f) + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 - tensor2 } + + assertEquals(shape, result.shape) + assertEquals(4f, result[0, 0]) // 5 - 1 + assertEquals(4f, result[0, 1]) // 6 - 2 + assertEquals(4f, result[1, 0]) // 7 - 3 + assertEquals(4f, result[1, 1]) // 8 - 4 + } + + @Test + fun testTensorMultiplication() { + val shape = Shape(2, 2) + val data1 = floatArrayOf(2f, 3f, 4f, 5f) + val data2 = floatArrayOf(3f, 4f, 5f, 6f) + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(6f, result[0, 0]) // 2 * 3 + assertEquals(12f, result[0, 1]) // 3 * 4 + assertEquals(20f, result[1, 0]) // 4 * 5 + assertEquals(30f, result[1, 1]) // 5 * 6 + } + + @Test + fun testTensorDivision() { + val shape = Shape(2, 2) + val data1 = floatArrayOf(6f, 8f, 10f, 12f) + val data2 = floatArrayOf(2f, 4f, 5f, 6f) + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 / tensor2 } + + assertEquals(shape, result.shape) + assertEquals(3f, result[0, 0]) // 6 / 2 + assertEquals(2f, result[0, 1]) // 8 / 4 + assertEquals(2f, result[1, 0]) // 10 / 5 + assertEquals(2f, result[1, 1]) // 12 / 6 + } + + @Test + fun testMatrixMultiplication() { + // Test 2x3 * 3x2 = 2x2 matrix multiplication + val shape1 = Shape(2, 3) + val shape2 = Shape(3, 2) + val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) // 2x3 matrix + val data2 = floatArrayOf(7f, 8f, 9f, 10f, 11f, 12f) // 3x2 matrix + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + val result = backend.matmul(tensor1, tensor2) + + assertEquals(Shape(2, 2), result.shape) + // First row: [1*7 + 2*9 + 3*11, 1*8 + 2*10 + 3*12] = [58, 64] + // Second row: [4*7 + 5*9 + 6*11, 4*8 + 5*10 + 6*12] = [139, 154] + assertEquals(58f, result[0, 0]) + assertEquals(64f, result[0, 1]) + assertEquals(139f, result[1, 0]) + assertEquals(154f, result[1, 1]) + } + + @Test + fun testDotProduct() { + val shape = Shape(3) + val data1 = floatArrayOf(1f, 2f, 3f) + val data2 = floatArrayOf(4f, 5f, 6f) + + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = backend.dot(tensor1, tensor2) + + // 1*4 + 2*5 + 3*6 = 4 + 10 + 18 = 32 + assertEquals(32.0, result, 1e-6) + } + + @Test + fun testScalarOperations() { + val shape = Shape(2, 2) + val data = floatArrayOf(1f, 2f, 3f, 4f) + val tensor = CpuTensorFP32.fromArray(shape, data) + + // Test tensor + scalar + val addResult = with(backend) { tensor + 5 } + assertEquals(6f, addResult[0, 0]) + assertEquals(7f, addResult[0, 1]) + assertEquals(8f, addResult[1, 0]) + assertEquals(9f, addResult[1, 1]) + + // Test tensor * scalar + val mulResult = with(backend) { tensor * 2f } + assertEquals(2f, mulResult[0, 0]) + assertEquals(4f, mulResult[0, 1]) + assertEquals(6f, mulResult[1, 0]) + assertEquals(8f, mulResult[1, 1]) + + // Test scalar * tensor + val scalarMulResult = with(backend) { 3.0 * tensor } + assertEquals(3f, scalarMulResult[0, 0]) + assertEquals(6f, scalarMulResult[0, 1]) + assertEquals(9f, scalarMulResult[1, 0]) + assertEquals(12f, scalarMulResult[1, 1]) + } + + @Test + fun testScale() { + val shape = Shape(2, 2) + val data = floatArrayOf(1f, 2f, 3f, 4f) + val tensor = CpuTensorFP32.fromArray(shape, data) + + val result = backend.scale(tensor, 2.5) + + assertEquals(2.5f, result[0, 0]) + assertEquals(5.0f, result[0, 1]) + assertEquals(7.5f, result[1, 0]) + assertEquals(10.0f, result[1, 1]) + } + + @Test + fun testInvalidDimensions() { + // Test that 5D tensor creation fails + val shape5D = Shape(1, 2, 3, 4, 5) + val data = FloatArray(1 * 2 * 3 * 4 * 5) { it.toFloat() } + + assertFailsWith { + CpuTensorFP32.fromArray(shape5D, data) + } + + // Test that 0D tensor creation fails + val shape0D = Shape() + val data0D = floatArrayOf(1f) + + assertFailsWith { + CpuTensorFP32.fromArray(shape0D, data0D) + } + } + + @Test + fun testDataSizeMismatch() { + val shape = Shape(2, 3) + val wrongSizeData = floatArrayOf(1f, 2f, 3f, 4f) // Should be 6 elements, not 4 + + assertFailsWith { + CpuTensorFP32.fromArray(shape, wrongSizeData) + } + } + + @Test + fun testMatrixMultiplicationInvalidDimensions() { + val shape1 = Shape(2, 3) + val shape2 = Shape(4, 2) // Should be 3x2 for valid multiplication + val data1 = FloatArray(6) { it.toFloat() } + val data2 = FloatArray(8) { it.toFloat() } + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + assertFailsWith { + backend.matmul(tensor1, tensor2) + } + } + + @Test + fun testTensorOperationsShapeMismatch() { + val shape1 = Shape(2, 3) + val shape2 = Shape(3, 2) + val data1 = FloatArray(6) { it.toFloat() } + val data2 = FloatArray(6) { it.toFloat() } + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + assertFailsWith { + with(backend) { tensor1 + tensor2 } + } + + assertFailsWith { + with(backend) { tensor1 - tensor2 } + } + + assertFailsWith { + with(backend) { tensor1 * tensor2 } + } + + assertFailsWith { + with(backend) { tensor1 / tensor2 } + } + } + + @Test + fun testNCHWLayoutPreservation() { + // Test that NCHW layout is preserved through operations + val shape = Shape(2, 3, 2, 2) // 2 batches, 3 channels, 2x2 spatial + val data = FloatArray(24) { it.toFloat() + 1f } + val tensor = CpuTensorFP32.fromArray(shape, data) + + // Test that indexing follows NCHW order + assertEquals(1f, tensor[0, 0, 0, 0]) // First element + assertEquals(2f, tensor[0, 0, 0, 1]) // Next in width + assertEquals(3f, tensor[0, 0, 1, 0]) // Next in height + assertEquals(4f, tensor[0, 0, 1, 1]) // Complete first channel + assertEquals(5f, tensor[0, 1, 0, 0]) // First element of second channel + assertEquals(13f, tensor[1, 0, 0, 0]) // First element of second batch + + // Test that operations preserve layout + val doubled = with(backend) { tensor * 2f } + assertEquals(2f, doubled[0, 0, 0, 0]) + assertEquals(4f, doubled[0, 0, 0, 1]) + assertEquals(6f, doubled[0, 0, 1, 0]) + assertEquals(10f, doubled[0, 1, 0, 0]) + assertEquals(26f, doubled[1, 0, 0, 0]) + } +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/EdgeCasesTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/EdgeCasesTest.kt new file mode 100644 index 000000000..49e2305a2 --- /dev/null +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/EdgeCasesTest.kt @@ -0,0 +1,320 @@ +package sk.ai.net.core.tensor.multiplication + +import sk.ai.net.core.tensor.* +import sk.ai.net.core.tensor.backend.* +import kotlin.test.* + +class EdgeCasesTest { + + private val backend = CpuBackend() + + @Test + fun testMinimalDimensionMultiplication() { + // Test 1x1 matrix multiplication + val shape1 = Shape(1, 1) + val shape2 = Shape(1, 1) + val data1 = floatArrayOf(5f) + val data2 = floatArrayOf(3f) + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + val result = backend.matmul(tensor1, tensor2) + + assertEquals(Shape(1, 1), result.shape) + assertEquals(15f, result[0, 0]) // 5 * 3 = 15 + } + + @Test + fun testSingleElementVectorMultiplication() { + // Test 1x1 * 1x1 as vector operations + val vector1Shape = Shape(1) + val vector2Shape = Shape(1) + val data1 = floatArrayOf(7f) + val data2 = floatArrayOf(2f) + + val vector1 = CpuTensorFP32.fromArray(vector1Shape, data1) + val vector2 = CpuTensorFP32.fromArray(vector2Shape, data2) + + // Element-wise multiplication + val elementResult = with(backend) { vector1 * vector2 } + assertEquals(Shape(1), elementResult.shape) + assertEquals(14f, elementResult[0]) // 7 * 2 = 14 + + // Dot product + val dotResult = backend.dot(vector1, vector2) + assertEquals(14.0, dotResult, 1e-6) // 7 * 2 = 14 + } + + @Test + fun testVeryLargeNumbers() { + val shape = Shape(2, 2) + val data1 = floatArrayOf(1e6f, 2e6f, 3e6f, 4e6f) + val data2 = floatArrayOf(5e6f, 6e6f, 7e6f, 8e6f) + + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(5e12f, result[0, 0], 1e6f) // 1e6 * 5e6 = 5e12 + assertEquals(12e12f, result[0, 1], 1e6f) // 2e6 * 6e6 = 12e12 + assertEquals(21e12f, result[1, 0], 1e6f) // 3e6 * 7e6 = 21e12 + assertEquals(32e12f, result[1, 1], 1e6f) // 4e6 * 8e6 = 32e12 + } + + @Test + fun testVerySmallNumbers() { + val shape = Shape(2, 2) + val data1 = floatArrayOf(1e-6f, 2e-6f, 3e-6f, 4e-6f) + val data2 = floatArrayOf(5e-6f, 6e-6f, 7e-6f, 8e-6f) + + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(5e-12f, result[0, 0], 1e-15f) // 1e-6 * 5e-6 = 5e-12 + assertEquals(12e-12f, result[0, 1], 1e-15f) // 2e-6 * 6e-6 = 12e-12 + assertEquals(21e-12f, result[1, 0], 1e-15f) // 3e-6 * 7e-6 = 21e-12 + assertEquals(32e-12f, result[1, 1], 1e-15f) // 4e-6 * 8e-6 = 32e-12 + } + + @Test + fun testInfinityHandling() { + val shape = Shape(2, 2) + val data1 = floatArrayOf(Float.POSITIVE_INFINITY, 1f, 2f, 3f) + val data2 = floatArrayOf(2f, Float.NEGATIVE_INFINITY, 4f, 5f) + + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(Float.POSITIVE_INFINITY, result[0, 0]) // +∞ * 2 = +∞ + assertEquals(Float.NEGATIVE_INFINITY, result[0, 1]) // 1 * -∞ = -∞ + assertEquals(8f, result[1, 0]) // 2 * 4 = 8 + assertEquals(15f, result[1, 1]) // 3 * 5 = 15 + } + + @Test + fun testNaNHandling() { + val shape = Shape(2, 2) + val data1 = floatArrayOf(Float.NaN, 1f, 2f, 3f) + val data2 = floatArrayOf(2f, Float.NaN, 4f, 5f) + + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertTrue(result[0, 0].isNaN()) // NaN * 2 = NaN + assertTrue(result[0, 1].isNaN()) // 1 * NaN = NaN + assertEquals(8f, result[1, 0]) // 2 * 4 = 8 + assertEquals(15f, result[1, 1]) // 3 * 5 = 15 + } + + @Test + fun testZeroDivisionInElementwise() { + val shape = Shape(2, 2) + val data1 = floatArrayOf(1f, 2f, 3f, 4f) + val data2 = floatArrayOf(0f, 2f, 0f, 4f) + + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 / tensor2 } + + assertEquals(shape, result.shape) + assertEquals(Float.POSITIVE_INFINITY, result[0, 0]) // 1 / 0 = +∞ + assertEquals(1f, result[0, 1]) // 2 / 2 = 1 + assertEquals(Float.POSITIVE_INFINITY, result[1, 0]) // 3 / 0 = +∞ + assertEquals(1f, result[1, 1]) // 4 / 4 = 1 + } + + @Test + fun testNegativeZeroDivision() { + val shape = Shape(2, 1) + val data1 = floatArrayOf(-1f, 1f) + val data2 = floatArrayOf(0f, 0f) + + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 / tensor2 } + + assertEquals(shape, result.shape) + assertEquals(Float.NEGATIVE_INFINITY, result[0, 0]) // -1 / 0 = -∞ + assertEquals(Float.POSITIVE_INFINITY, result[1, 0]) // 1 / 0 = +∞ + } + + @Test + fun testOverflowInMatrixMultiplication() { + // Test potential overflow in matrix multiplication + val shape1 = Shape(2, 2) + val shape2 = Shape(2, 2) + val data1 = floatArrayOf(Float.MAX_VALUE / 2, 0f, 0f, Float.MAX_VALUE / 2) + val data2 = floatArrayOf(3f, 0f, 0f, 3f) + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + val result = backend.matmul(tensor1, tensor2) + + assertEquals(shape1, result.shape) + // Should overflow to infinity + assertEquals(Float.POSITIVE_INFINITY, result[0, 0]) + assertEquals(0f, result[0, 1]) + assertEquals(0f, result[1, 0]) + assertEquals(Float.POSITIVE_INFINITY, result[1, 1]) + } + + @Test + fun testUnderflowInMultiplication() { + val shape = Shape(2, 2) + val data1 = floatArrayOf(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE) + val data2 = floatArrayOf(0.5f, 0.5f, 0.5f, 0.5f) + + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + // Results should be extremely small or zero due to underflow + assertTrue(result[0, 0] >= 0f) + assertTrue(result[0, 1] >= 0f) + assertTrue(result[1, 0] >= 0f) + assertTrue(result[1, 1] >= 0f) + } + + @Test + fun testMismatchedDimensionsElementwise() { + val shape1 = Shape(3, 2) + val shape2 = Shape(2, 3) + val data1 = FloatArray(6) { it.toFloat() } + val data2 = FloatArray(6) { it.toFloat() } + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + assertFailsWith { + with(backend) { tensor1 * tensor2 } + } + + assertFailsWith { + with(backend) { tensor1 + tensor2 } + } + + assertFailsWith { + with(backend) { tensor1 - tensor2 } + } + + assertFailsWith { + with(backend) { tensor1 / tensor2 } + } + } + + @Test + fun testIncompatibleMatrixDimensions() { + val shape1 = Shape(3, 4) + val shape2 = Shape(5, 2) // Should be 4xN for compatibility + val data1 = FloatArray(12) { it.toFloat() } + val data2 = FloatArray(10) { it.toFloat() } + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + assertFailsWith { + backend.matmul(tensor1, tensor2) + } + } + + @Test + fun testNon2DMatrixMultiplication() { + val shape1 = Shape(2, 3, 4) // 3D tensor + val shape2 = Shape(4, 5) // 2D tensor + val data1 = FloatArray(24) { it.toFloat() } + val data2 = FloatArray(20) { it.toFloat() } + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + assertFailsWith { + backend.matmul(tensor1, tensor2) + } + } + + @Test + fun testDotProductDimensionMismatch() { + val shape1 = Shape(3) + val shape2 = Shape(4) + val data1 = FloatArray(3) { it.toFloat() } + val data2 = FloatArray(4) { it.toFloat() } + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + assertFailsWith { + backend.dot(tensor1, tensor2) + } + } + + @Test + fun testMultiplicationWithEmptyShapes() { + // Test behavior with minimal valid shapes + val shape1 = Shape(1) + val shape2 = Shape(1) + val data1 = floatArrayOf(5f) + val data2 = floatArrayOf(3f) + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + val result = with(backend) { tensor1 * tensor2 } + assertEquals(Shape(1), result.shape) + assertEquals(15f, result[0]) + } + + @Test + fun testMaxDimensionTensors() { + // Test with 4D tensors (maximum supported) + val shape = Shape(2, 2, 2, 2) + val data1 = FloatArray(16) { (it + 1).toFloat() } + val data2 = FloatArray(16) { (it % 3 + 1).toFloat() } + + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(1f, result[0, 0, 0, 0]) // 1 * 1 = 1 + assertEquals(4f, result[0, 0, 0, 1]) // 2 * 2 = 4 + assertEquals(9f, result[0, 0, 1, 0]) // 3 * 3 = 9 + } + + @Test + fun testPrecisionLossInLargeMultiplications() { + // Test precision with operations that might lose precision + val shape = Shape(1000) + val data1 = FloatArray(1000) { 1.0000001f } + val data2 = FloatArray(1000) { 1.0000001f } + + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + // All results should be close to 1.0000002f (1.0000001 * 1.0000001) + for (i in 0 until 1000) { + assertTrue(result[i] > 1f) + assertTrue(result[i] < 1.1f) // Should be very close to 1 + } + } +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/ElementwiseMultiplicationTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/ElementwiseMultiplicationTest.kt new file mode 100644 index 000000000..69ebf4582 --- /dev/null +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/ElementwiseMultiplicationTest.kt @@ -0,0 +1,192 @@ +package sk.ai.net.core.tensor.multiplication + +import sk.ai.net.core.tensor.* +import sk.ai.net.core.tensor.backend.* +import kotlin.test.* + +class ElementwiseMultiplicationTest { + + private val backend = CpuBackend() + + @Test + fun testBasicElementwiseMultiplication() { + val shape = Shape(2, 3) + val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) + val data2 = floatArrayOf(2f, 3f, 4f, 5f, 6f, 7f) + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(2f, result[0, 0]) // 1 * 2 + assertEquals(6f, result[0, 1]) // 2 * 3 + assertEquals(12f, result[0, 2]) // 3 * 4 + assertEquals(20f, result[1, 0]) // 4 * 5 + assertEquals(30f, result[1, 1]) // 5 * 6 + assertEquals(42f, result[1, 2]) // 6 * 7 + } + + @Test + fun testElementwiseMultiplication1D() { + val shape = Shape(5) + val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f) + val data2 = floatArrayOf(2f, 3f, 4f, 5f, 6f) + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(2f, result[0]) // 1 * 2 + assertEquals(6f, result[1]) // 2 * 3 + assertEquals(12f, result[2]) // 3 * 4 + assertEquals(20f, result[3]) // 4 * 5 + assertEquals(30f, result[4]) // 5 * 6 + } + + @Test + fun testElementwiseMultiplication3D() { + val shape = Shape(2, 2, 2) + val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f) + val data2 = floatArrayOf(2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f) + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(2f, result[0, 0, 0]) // 1 * 2 + assertEquals(6f, result[0, 0, 1]) // 2 * 3 + assertEquals(12f, result[0, 1, 0]) // 3 * 4 + assertEquals(20f, result[0, 1, 1]) // 4 * 5 + assertEquals(30f, result[1, 0, 0]) // 5 * 6 + assertEquals(42f, result[1, 0, 1]) // 6 * 7 + assertEquals(56f, result[1, 1, 0]) // 7 * 8 + assertEquals(72f, result[1, 1, 1]) // 8 * 9 + } + + @Test + fun testElementwiseMultiplication4D() { + // NCHW format: 2 batches, 1 channel, 2x2 spatial + val shape = Shape(2, 1, 2, 2) + val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f) + val data2 = floatArrayOf(2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f) + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(2f, result[0, 0, 0, 0]) // 1 * 2 + assertEquals(6f, result[0, 0, 0, 1]) // 2 * 3 + assertEquals(12f, result[0, 0, 1, 0]) // 3 * 4 + assertEquals(20f, result[0, 0, 1, 1]) // 4 * 5 + assertEquals(30f, result[1, 0, 0, 0]) // 5 * 6 + assertEquals(42f, result[1, 0, 0, 1]) // 6 * 7 + assertEquals(56f, result[1, 0, 1, 0]) // 7 * 8 + assertEquals(72f, result[1, 0, 1, 1]) // 8 * 9 + } + + @Test + fun testElementwiseWithZeros() { + val shape = Shape(3, 3) + val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f) + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.zeros(shape) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + for (i in 0 until 3) { + for (j in 0 until 3) { + assertEquals(0f, result[i, j]) + } + } + } + + @Test + fun testElementwiseWithOnes() { + val shape = Shape(2, 4) + val data = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f) + val tensor1 = CpuTensorFP32.fromArray(shape, data) + val tensor2 = CpuTensorFP32.ones(shape) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(1f, result[0, 0]) + assertEquals(2f, result[0, 1]) + assertEquals(3f, result[0, 2]) + assertEquals(4f, result[0, 3]) + assertEquals(5f, result[1, 0]) + assertEquals(6f, result[1, 1]) + assertEquals(7f, result[1, 2]) + assertEquals(8f, result[1, 3]) + } + + @Test + fun testElementwiseWithNegativeValues() { + val shape = Shape(2, 2) + val data1 = floatArrayOf(-1f, 2f, -3f, 4f) + val data2 = floatArrayOf(2f, -3f, 4f, -5f) + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(-2f, result[0, 0]) // -1 * 2 = -2 + assertEquals(-6f, result[0, 1]) // 2 * -3 = -6 + assertEquals(-12f, result[1, 0]) // -3 * 4 = -12 + assertEquals(-20f, result[1, 1]) // 4 * -5 = -20 + } + + @Test + fun testElementwiseWithFractionalValues() { + val shape = Shape(2, 2) + val data1 = floatArrayOf(0.5f, 1.5f, 2.5f, 3.5f) + val data2 = floatArrayOf(2f, 4f, 6f, 8f) + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(1f, result[0, 0], 1e-6f) // 0.5 * 2 = 1 + assertEquals(6f, result[0, 1], 1e-6f) // 1.5 * 4 = 6 + assertEquals(15f, result[1, 0], 1e-6f) // 2.5 * 6 = 15 + assertEquals(28f, result[1, 1], 1e-6f) // 3.5 * 8 = 28 + } + + @Test + fun testLargeElementwiseMultiplication() { + val shape = Shape(100, 100) + val data1 = FloatArray(10000) { (it + 1).toFloat() } + val data2 = FloatArray(10000) { (it * 2).toFloat() } + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = with(backend) { tensor1 * tensor2 } + + assertEquals(shape, result.shape) + assertEquals(0f, result[0, 0]) // 1 * 0 = 0 + assertEquals(4f, result[0, 1]) // 2 * 2 = 4 + assertEquals(12f, result[0, 2]) // 3 * 4 = 12 + assertEquals(199980000f, result[99, 99], 1000f) // 10000 * 19998 = 199980000 + } + + @Test + fun testElementwiseShapeMismatch() { + val shape1 = Shape(2, 3) + val shape2 = Shape(3, 2) + val data1 = FloatArray(6) { it.toFloat() } + val data2 = FloatArray(6) { it.toFloat() } + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + assertFailsWith { + with(backend) { tensor1 * tensor2 } + } + } +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MLOperationsTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MLOperationsTest.kt new file mode 100644 index 000000000..cf82958d7 --- /dev/null +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MLOperationsTest.kt @@ -0,0 +1,379 @@ +package sk.ai.net.core.tensor.multiplication + +import sk.ai.net.core.tensor.* +import sk.ai.net.core.tensor.backend.* +import kotlin.test.* + +class MLOperationsTest { + + private val backend = CpuBackend() + + @Test + fun testLinearLayerForward() { + // Simulate a linear layer: input * weights + bias + // Input: batch_size=2, features=3 + // Weights: features=3, output_features=4 + val inputShape = Shape(2, 3) // 2 samples, 3 features each + val weightShape = Shape(3, 4) // 3 input features, 4 output features + + val input = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) // 2x3 input batch + val weights = FloatArray(12) { (it + 1).toFloat() } // 3x4 weight matrix + + val inputTensor = CpuTensorFP32.fromArray(inputShape, input) + val weightTensor = CpuTensorFP32.fromArray(weightShape, weights) + + val result = backend.matmul(inputTensor, weightTensor) + + assertEquals(Shape(2, 4), result.shape) // 2 samples, 4 output features + + // First sample output: [1*1 + 2*5 + 3*9, 1*2 + 2*6 + 3*10, 1*3 + 2*7 + 3*11, 1*4 + 2*8 + 3*12] + // = [38, 44, 50, 56] + assertEquals(38f, result[0, 0]) + assertEquals(44f, result[0, 1]) + assertEquals(50f, result[0, 2]) + assertEquals(56f, result[0, 3]) + + // Second sample output: [4*1 + 5*5 + 6*9, 4*2 + 5*6 + 6*10, 4*3 + 5*7 + 6*11, 4*4 + 5*8 + 6*12] + // = [83, 98, 113, 128] + assertEquals(83f, result[1, 0]) + assertEquals(98f, result[1, 1]) + assertEquals(113f, result[1, 2]) + assertEquals(128f, result[1, 3]) + } + + @Test + fun testConvolutionalWeightMultiplication() { + // Simulate flattened convolution operation + // Feature map: NCHW format - 1 batch, 2 channels, 3x3 spatial + val featureShape = Shape(1, 2, 3, 3) // 1x2x3x3 + val filterShape = Shape(18, 8) // Flattened: 2*3*3=18 input features, 8 filters + + val featureData = FloatArray(18) { (it + 1).toFloat() } + val filterData = FloatArray(144) { (it % 5 + 1).toFloat() } + + // Flatten feature map for matrix multiplication + val flatFeatureShape = Shape(1, 18) // Batch size 1, 18 features + val flatFeature = CpuTensorFP32.fromArray(flatFeatureShape, featureData) + val filters = CpuTensorFP32.fromArray(filterShape, filterData) + + val result = backend.matmul(flatFeature, filters) + + assertEquals(Shape(1, 8), result.shape) // 1 batch, 8 output channels + assertTrue(result[0, 0] > 0f) + assertTrue(result[0, 7] > 0f) + } + + @Test + fun testBatchedLinearTransformation() { + // Batch processing: multiple samples through the same linear layer + val batchSize = 5 + val inputFeatures = 10 + val outputFeatures = 7 + + val inputShape = Shape(batchSize, inputFeatures) + val weightShape = Shape(inputFeatures, outputFeatures) + + val inputData = FloatArray(50) { (it % 3 + 1).toFloat() } + val weightData = FloatArray(70) { (it % 4 + 1).toFloat() } + + val input = CpuTensorFP32.fromArray(inputShape, inputData) + val weights = CpuTensorFP32.fromArray(weightShape, weightData) + + val result = backend.matmul(input, weights) + + assertEquals(Shape(batchSize, outputFeatures), result.shape) + + // Each sample should produce different outputs + assertNotEquals(result[0, 0], result[1, 0]) + assertNotEquals(result[0, 0], result[2, 0]) + } + + @Test + fun testAttentionWeightMultiplication() { + // Simulate attention mechanism: Q * K^T + val sequenceLength = 8 + val embeddingDim = 16 + + val queryShape = Shape(sequenceLength, embeddingDim) // 8x16 + val keyShape = Shape(embeddingDim, sequenceLength) // 16x8 (transposed) + + val queryData = FloatArray(128) { (it % 7 + 1).toFloat() } + val keyData = FloatArray(128) { (it % 5 + 1).toFloat() } + + val query = CpuTensorFP32.fromArray(queryShape, queryData) + val key = CpuTensorFP32.fromArray(keyShape, keyData) + + val attentionScores = backend.matmul(query, key) + + assertEquals(Shape(sequenceLength, sequenceLength), attentionScores.shape) // 8x8 + + // Attention scores should be symmetric in terms of dimensions + assertTrue(attentionScores[0, 0] > 0f) + assertTrue(attentionScores[7, 7] > 0f) + } + + @Test + fun testEmbeddingLookup() { + // Simulate embedding lookup as matrix multiplication with one-hot vectors + val vocabSize = 1000 + val embeddingDim = 128 + val batchSize = 3 + + // One-hot encoded input (simplified - only test a few positions) + val oneHotShape = Shape(batchSize, vocabSize) + val embeddingShape = Shape(vocabSize, embeddingDim) + + // Create sparse one-hot vectors (mostly zeros with single 1s) + val oneHotData = FloatArray(3000) { 0f } + oneHotData[0] = 1f // First sample: word at position 0 + oneHotData[1001] = 1f // Second sample: word at position 1 + oneHotData[2002] = 1f // Third sample: word at position 2 + + val embeddingData = FloatArray(128000) { (it % 10 + 1).toFloat() } + + val oneHot = CpuTensorFP32.fromArray(oneHotShape, oneHotData) + val embeddings = CpuTensorFP32.fromArray(embeddingShape, embeddingData) + + val result = backend.matmul(oneHot, embeddings) + + assertEquals(Shape(batchSize, embeddingDim), result.shape) + + // Each sample should get different embeddings + assertNotEquals(result[0, 0], result[1, 0]) + assertNotEquals(result[1, 0], result[2, 0]) + } + + @Test + fun testMultiHeadAttentionProjection() { + // Multiple attention heads processing in parallel + val batchSize = 2 + val sequenceLength = 6 + val embeddingDim = 12 + val numHeads = 3 + val headDim = embeddingDim / numHeads // 4 + + val inputShape = Shape(batchSize * sequenceLength, embeddingDim) // 12x12 + val projectionShape = Shape(embeddingDim, embeddingDim) // 12x12 + + val inputData = FloatArray(144) { (it % 8 + 1).toFloat() } + val projectionData = FloatArray(144) { (it % 6 + 1).toFloat() } + + val input = CpuTensorFP32.fromArray(inputShape, inputData) + val projection = CpuTensorFP32.fromArray(projectionShape, projectionData) + + val projected = backend.matmul(input, projection) + + assertEquals(Shape(batchSize * sequenceLength, embeddingDim), projected.shape) + assertTrue(projected[0, 0] > 0f) + assertTrue(projected[11, 11] > 0f) + } + + @Test + fun testResidualConnectionWeights() { + // Test weight matrices used in residual connections + val hiddenSize = 256 + val intermediateSize = 1024 + val batchSize = 4 + + // Forward: hidden -> intermediate + val inputShape = Shape(batchSize, hiddenSize) + val forwardWeightShape = Shape(hiddenSize, intermediateSize) + + val input = FloatArray(1024) { (it % 12 + 1).toFloat() } + val forwardWeights = FloatArray(262144) { (it % 7 + 1).toFloat() } + + val inputTensor = CpuTensorFP32.fromArray(inputShape, input) + val forwardTensor = CpuTensorFP32.fromArray(forwardWeightShape, forwardWeights) + + val intermediate = backend.matmul(inputTensor, forwardTensor) + + assertEquals(Shape(batchSize, intermediateSize), intermediate.shape) + + // Backward: intermediate -> hidden + val backwardWeightShape = Shape(intermediateSize, hiddenSize) + val backwardWeights = FloatArray(262144) { (it % 9 + 1).toFloat() } + val backwardTensor = CpuTensorFP32.fromArray(backwardWeightShape, backwardWeights) + + val output = backend.matmul(intermediate, backwardTensor) + + assertEquals(Shape(batchSize, hiddenSize), output.shape) + assertTrue(output[0, 0] > 0f) + assertTrue(output[3, 255] > 0f) + } + + @Test + fun testGradientAccumulation() { + // Simulate gradient computation through matrix multiplication + val batchSize = 8 + val features = 64 + val outputs = 32 + + val activationShape = Shape(batchSize, features) + val gradientShape = Shape(batchSize, outputs) + val weightGradientShape = Shape(features, outputs) + + val activations = FloatArray(512) { (it % 5 + 1).toFloat() } + val outputGradients = FloatArray(256) { (it % 3 + 1).toFloat() } + + val activationTensor = CpuTensorFP32.fromArray(activationShape, activations) + val gradientTensor = CpuTensorFP32.fromArray(gradientShape, outputGradients) + + // Transpose activations for gradient computation: A^T * grad_output + val transposeShape = Shape(features, batchSize) + val transposedActivations = FloatArray(512) + + // Manual transpose for testing + for (i in 0 until batchSize) { + for (j in 0 until features) { + transposedActivations[j * batchSize + i] = activations[i * features + j] + } + } + + val transposedTensor = CpuTensorFP32.fromArray(transposeShape, transposedActivations) + val weightGradients = backend.matmul(transposedTensor, gradientTensor) + + assertEquals(weightGradientShape, weightGradients.shape) + assertTrue(weightGradients[0, 0] > 0f) + assertTrue(weightGradients[63, 31] > 0f) + } + + @Test + fun testBatchNormalizationStatistics() { + // Compute batch statistics using matrix operations + val batchSize = 16 + val channels = 64 + val spatialSize = 49 // 7x7 + + // Reshape from NCHW to matrix for batch norm computation + val inputShape = Shape(batchSize, channels * spatialSize) + val statisticsWeightShape = Shape(channels * spatialSize, channels) + + val inputData = FloatArray(batchSize * channels * spatialSize) { (it % 11 + 1).toFloat() } + val weightData = FloatArray(channels * spatialSize * channels) { if (it % (spatialSize + 1) == 0) 1f else 0f } + + val input = CpuTensorFP32.fromArray(inputShape, inputData) + val weights = CpuTensorFP32.fromArray(statisticsWeightShape, weightData) + + val channelSums = backend.matmul(input, weights) + + assertEquals(Shape(batchSize, channels), channelSums.shape) + assertTrue(channelSums[0, 0] > 0f) + } + + @Test + fun testLargeScaleMLMultiplication() { + // Test large matrix multiplication similar to modern ML models + val batchSize = 32 + val sequenceLength = 512 + val hiddenSize = 768 + + // Typical transformer layer sizes + val inputShape = Shape(batchSize, sequenceLength * hiddenSize / 16) // Reduced for testing + val weightShape = Shape(sequenceLength * hiddenSize / 16, hiddenSize / 4) + + val inputData = FloatArray(inputShape.volume) { (it % 13 + 1).toFloat() } + val weightData = FloatArray(weightShape.volume) { (it % 17 + 1).toFloat() } + + val input = CpuTensorFP32.fromArray(inputShape, inputData) + val weights = CpuTensorFP32.fromArray(weightShape, weightData) + + val result = backend.matmul(input, weights) + + assertEquals(Shape(batchSize, hiddenSize / 4), result.shape) + assertTrue(result[0, 0] > 0f) + assertTrue(result[31, 191] > 0f) + } + + @Test + fun testMLPWithTensorList() { + // Test MLP with 1 input, 2 hidden layers (16 neurons each), 1 output using list of tensors + val batchSize = 4 + val inputSize = 1 + val hiddenSize = 16 + val outputSize = 1 + + // Create input data (4 samples, 1 feature each) + val inputData = floatArrayOf(1.0f, 2.0f, 3.0f, 4.0f) + val input = CpuTensorFP32.fromArray(Shape(batchSize, inputSize), inputData) + + // Create list of weight tensors for MLP layers + val weights = mutableListOf() + val biases = mutableListOf() + + // Layer 1: input_size -> hidden_size (1 -> 16) + val w1Data = FloatArray(inputSize * hiddenSize) { (it % 5 + 1).toFloat() / 10f } // Small weights + val b1Data = FloatArray(batchSize * hiddenSize) { 0.1f } // Bias for each batch + weights.add(CpuTensorFP32.fromArray(Shape(inputSize, hiddenSize), w1Data)) + biases.add(CpuTensorFP32.fromArray(Shape(batchSize, hiddenSize), b1Data)) + + // Layer 2: hidden_size -> hidden_size (16 -> 16) + val w2Data = FloatArray(hiddenSize * hiddenSize) { (it % 3 + 1).toFloat() / 20f } // Small weights + val b2Data = FloatArray(batchSize * hiddenSize) { 0.05f } + weights.add(CpuTensorFP32.fromArray(Shape(hiddenSize, hiddenSize), w2Data)) + biases.add(CpuTensorFP32.fromArray(Shape(batchSize, hiddenSize), b2Data)) + + // Layer 3: hidden_size -> output_size (16 -> 1) + val w3Data = FloatArray(hiddenSize * outputSize) { (it % 7 + 1).toFloat() / 30f } // Small weights + val b3Data = FloatArray(batchSize * outputSize) { 0.0f } + weights.add(CpuTensorFP32.fromArray(Shape(hiddenSize, outputSize), w3Data)) + biases.add(CpuTensorFP32.fromArray(Shape(batchSize, outputSize), b3Data)) + + // Forward pass through MLP + var activation: Tensor = input + + // Layer 1: input -> hidden1 + activation = backend.matmul(activation, weights[0]) + // Add bias using tensor addition + with(backend) { + activation = activation.plus(biases[0]) + } + + // Apply ReLU activation (max(0, x)) + val relu1Data = FloatArray(activation.shape.volume) + for (i in relu1Data.indices) { + relu1Data[i] = maxOf(0f, (activation as CpuTensorFP32).data[i]) + } + activation = CpuTensorFP32.fromArray(activation.shape, relu1Data) + + // Layer 2: hidden1 -> hidden2 + activation = backend.matmul(activation, weights[1]) + // Add bias using tensor addition + with(backend) { + activation = activation.plus(biases[1]) + } + + // Apply ReLU activation + val relu2Data = FloatArray(activation.shape.volume) + for (i in relu2Data.indices) { + relu2Data[i] = maxOf(0f, (activation as CpuTensorFP32).data[i]) + } + activation = CpuTensorFP32.fromArray(activation.shape, relu2Data) + + // Layer 3: hidden2 -> output + val output = backend.matmul(activation, weights[2]) + + // Verify MLP structure + assertEquals(3, weights.size, "Should have 3 weight matrices") + assertEquals(3, biases.size, "Should have 3 bias vectors") + + // Verify layer shapes + assertEquals(Shape(inputSize, hiddenSize), weights[0].shape, "Layer 1 weight shape") + assertEquals(Shape(hiddenSize, hiddenSize), weights[1].shape, "Layer 2 weight shape") + assertEquals(Shape(hiddenSize, outputSize), weights[2].shape, "Layer 3 weight shape") + + // Verify output shape + assertEquals(Shape(batchSize, outputSize), output.shape, "Output should be batch_size x output_size") + + // Verify output values are reasonable (not NaN or infinite) + for (i in 0 until batchSize) { + val outputValue = output[i, 0] + assertTrue(outputValue.isFinite(), "Output should be finite") + println("[DEBUG_LOG] Sample $i: input=${inputData[i]}, output=$outputValue") + } + + // Verify different inputs produce different outputs + assertNotEquals(output[0, 0], output[1, 0], "Different inputs should produce different outputs") + + println("[DEBUG_LOG] MLP test completed successfully with ${weights.size} layers") + } +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MatrixMultiplicationTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MatrixMultiplicationTest.kt new file mode 100644 index 000000000..19a266b1f --- /dev/null +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MatrixMultiplicationTest.kt @@ -0,0 +1,237 @@ +package sk.ai.net.core.tensor.multiplication + +import sk.ai.net.core.tensor.* +import sk.ai.net.core.tensor.backend.* +import kotlin.test.* + +class MatrixMultiplicationTest { + + private val backend = CpuBackend() + + @Test + fun testBasicMatrixMultiplication() { + val shape1 = Shape(2, 3) + val shape2 = Shape(3, 2) + val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) // 2x3 matrix + val data2 = floatArrayOf(7f, 8f, 9f, 10f, 11f, 12f) // 3x2 matrix + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + val result = backend.matmul(tensor1, tensor2) + + assertEquals(Shape(2, 2), result.shape) + // First row: [1*7 + 2*9 + 3*11, 1*8 + 2*10 + 3*12] = [58, 64] + // Second row: [4*7 + 5*9 + 6*11, 4*8 + 5*10 + 6*12] = [139, 154] + assertEquals(58f, result[0, 0]) + assertEquals(64f, result[0, 1]) + assertEquals(139f, result[1, 0]) + assertEquals(154f, result[1, 1]) + } + + @Test + fun testSquareMatrixMultiplication() { + val shape = Shape(3, 3) + val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f) + val data2 = floatArrayOf(9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f) + + val tensor1 = CpuTensorFP32.fromArray(shape, data1) + val tensor2 = CpuTensorFP32.fromArray(shape, data2) + + val result = backend.matmul(tensor1, tensor2) + + assertEquals(shape, result.shape) + // First row: [1*9 + 2*6 + 3*3, 1*8 + 2*5 + 3*2, 1*7 + 2*4 + 3*1] = [30, 24, 18] + assertEquals(30f, result[0, 0]) + assertEquals(24f, result[0, 1]) + assertEquals(18f, result[0, 2]) + // Second row: [4*9 + 5*6 + 6*3, 4*8 + 5*5 + 6*2, 4*7 + 5*4 + 6*1] = [84, 69, 54] + assertEquals(84f, result[1, 0]) + assertEquals(69f, result[1, 1]) + assertEquals(54f, result[1, 2]) + // Third row: [7*9 + 8*6 + 9*3, 7*8 + 8*5 + 9*2, 7*7 + 8*4 + 9*1] = [138, 114, 90] + assertEquals(138f, result[2, 0]) + assertEquals(114f, result[2, 1]) + assertEquals(90f, result[2, 2]) + } + + @Test + fun testIdentityMatrixMultiplication() { + val shape = Shape(3, 3) + val data = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f) + val identityData = floatArrayOf(1f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f) + + val tensor = CpuTensorFP32.fromArray(shape, data) + val identity = CpuTensorFP32.fromArray(shape, identityData) + + val result = backend.matmul(tensor, identity) + + assertEquals(shape, result.shape) + // Multiplying by identity should return the original matrix + assertEquals(1f, result[0, 0]) + assertEquals(2f, result[0, 1]) + assertEquals(3f, result[0, 2]) + assertEquals(4f, result[1, 0]) + assertEquals(5f, result[1, 1]) + assertEquals(6f, result[1, 2]) + assertEquals(7f, result[2, 0]) + assertEquals(8f, result[2, 1]) + assertEquals(9f, result[2, 2]) + } + + @Test + fun testVectorMatrixMultiplication() { + // 1x3 vector * 3x4 matrix = 1x4 vector + val shape1 = Shape(1, 3) + val shape2 = Shape(3, 4) + val data1 = floatArrayOf(2f, 3f, 4f) // row vector + val data2 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f) // 3x4 matrix + + val vector = CpuTensorFP32.fromArray(shape1, data1) + val matrix = CpuTensorFP32.fromArray(shape2, data2) + + val result = backend.matmul(vector, matrix) + + assertEquals(Shape(1, 4), result.shape) + // [2*1 + 3*5 + 4*9, 2*2 + 3*6 + 4*10, 2*3 + 3*7 + 4*11, 2*4 + 3*8 + 4*12] = [53, 62, 71, 80] + assertEquals(53f, result[0, 0]) + assertEquals(62f, result[0, 1]) + assertEquals(71f, result[0, 2]) + assertEquals(80f, result[0, 3]) + } + + @Test + fun testMatrixVectorMultiplication() { + // 4x3 matrix * 3x1 vector = 4x1 vector + val shape1 = Shape(4, 3) + val shape2 = Shape(3, 1) + val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f) // 4x3 matrix + val data2 = floatArrayOf(2f, 3f, 4f) // column vector + + val matrix = CpuTensorFP32.fromArray(shape1, data1) + val vector = CpuTensorFP32.fromArray(shape2, data2) + + val result = backend.matmul(matrix, vector) + + assertEquals(Shape(4, 1), result.shape) + // [1*2 + 2*3 + 3*4] = [20], [4*2 + 5*3 + 6*4] = [47], [7*2 + 8*3 + 9*4] = [74], [10*2 + 11*3 + 12*4] = [101] + assertEquals(20f, result[0, 0]) + assertEquals(47f, result[1, 0]) + assertEquals(74f, result[2, 0]) + assertEquals(101f, result[3, 0]) + } + + @Test + fun testLargeMatrixMultiplication() { + val shape1 = Shape(50, 30) + val shape2 = Shape(30, 40) + val data1 = FloatArray(1500) { (it % 10 + 1).toFloat() } + val data2 = FloatArray(1200) { (it % 5 + 1).toFloat() } + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + val result = backend.matmul(tensor1, tensor2) + + assertEquals(Shape(50, 40), result.shape) + // Verify specific elements by manual calculation + assertTrue(result[0, 0] > 0f) + assertTrue(result[49, 39] > 0f) + } + + @Test + fun testMatrixMultiplicationWithZeros() { + val shape1 = Shape(2, 3) + val shape2 = Shape(3, 2) + val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) + val zeros = CpuTensorFP32.zeros(shape2) + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + + val result = backend.matmul(tensor1, zeros) + + assertEquals(Shape(2, 2), result.shape) + assertEquals(0f, result[0, 0]) + assertEquals(0f, result[0, 1]) + assertEquals(0f, result[1, 0]) + assertEquals(0f, result[1, 1]) + } + + @Test + fun testMatrixMultiplicationWithNegatives() { + val shape1 = Shape(2, 2) + val shape2 = Shape(2, 2) + val data1 = floatArrayOf(-1f, 2f, -3f, 4f) + val data2 = floatArrayOf(5f, -6f, -7f, 8f) + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + val result = backend.matmul(tensor1, tensor2) + + assertEquals(shape1, result.shape) + // First row: [-1*5 + 2*(-7), -1*(-6) + 2*8] = [-19, 22] + // Second row: [-3*5 + 4*(-7), -3*(-6) + 4*8] = [-43, 50] + assertEquals(-19f, result[0, 0]) + assertEquals(22f, result[0, 1]) + assertEquals(-43f, result[1, 0]) + assertEquals(50f, result[1, 1]) + } + + @Test + fun testInvalidMatrixMultiplicationDimensions() { + val shape1 = Shape(2, 3) + val shape2 = Shape(4, 2) // Wrong inner dimension - should be 3x2 + val data1 = FloatArray(6) { it.toFloat() } + val data2 = FloatArray(8) { it.toFloat() } + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + assertFailsWith { + backend.matmul(tensor1, tensor2) + } + } + + @Test + fun testMatrixMultiplicationNonSquareResult() { + // Test rectangular result matrices + val shape1 = Shape(5, 3) + val shape2 = Shape(3, 7) + val data1 = FloatArray(15) { (it + 1).toFloat() } + val data2 = FloatArray(21) { (it % 3 + 1).toFloat() } + + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + + val result = backend.matmul(tensor1, tensor2) + + assertEquals(Shape(5, 7), result.shape) + // Verify the multiplication produces correct dimensions + assertTrue(result[0, 0] > 0f) + assertTrue(result[4, 6] > 0f) + } + + @Test + fun testMatrixChainMultiplication() { + // Test A * B * C through (A * B) * C + val shapeA = Shape(2, 3) + val shapeB = Shape(3, 4) + val shapeC = Shape(4, 2) + + val dataA = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) + val dataB = FloatArray(12) { (it + 1).toFloat() } + val dataC = FloatArray(8) { (it % 2 + 1).toFloat() } + + val tensorA = CpuTensorFP32.fromArray(shapeA, dataA) + val tensorB = CpuTensorFP32.fromArray(shapeB, dataB) + val tensorC = CpuTensorFP32.fromArray(shapeC, dataC) + + val resultAB = backend.matmul(tensorA, tensorB) + val resultABC = backend.matmul(resultAB, tensorC) + + assertEquals(Shape(2, 2), resultABC.shape) + assertTrue(resultABC[0, 0] > 0f) + assertTrue(resultABC[1, 1] > 0f) + } +} \ No newline at end of file From c22ad0f5b0436e08abe6042bfc1ed75542ecd26d Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 00:15:33 +0200 Subject: [PATCH 04/16] Add initial version of performance measurement Related-To: #100 --- .../benchmark/BaselinePerformanceTest.kt | 212 +++++++++++ .../core/tensor/benchmark/BenchmarkRunner.kt | 156 ++++++++ .../tensor/benchmark/TensorBenchmarkSuite.kt | 347 ++++++++++++++++++ 3 files changed, 715 insertions(+) create mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BaselinePerformanceTest.kt create mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BenchmarkRunner.kt create mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/TensorBenchmarkSuite.kt diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BaselinePerformanceTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BaselinePerformanceTest.kt new file mode 100644 index 000000000..402e614ab --- /dev/null +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BaselinePerformanceTest.kt @@ -0,0 +1,212 @@ +package sk.ai.net.core.tensor.benchmark + +import sk.ai.net.core.tensor.backend.CpuBackend +import kotlin.test.Test + +/** + * Baseline performance test demonstrating the benchmarking framework. + * This test measures the current performance of CpuBackend operations + * to establish baseline metrics for SIMD comparison. + */ +class BaselinePerformanceTest { + + private val backend = CpuBackend() + private val suite = TensorBenchmarkSuite(backend) + + @Test + fun testMatrixMultiplicationBaseline() { + println("\n=== MATRIX MULTIPLICATION BASELINE ===") + + // Test smaller sizes for quick baseline measurement + val testSizes = listOf( + Triple(32, 32, 32), + Triple(64, 64, 64), + Triple(128, 128, 128), + Triple(256, 256, 256) + ) + + val results = suite.benchmarkMatrixMultiplication(testSizes) + + results.forEach { result -> + println(result.prettyPrint()) + println() + } + + // Validate performance characteristics + println("Performance Analysis:") + results.forEach { result -> + val size = extractMatrixSize(result.name) + val operations = size * size * size.toLong() // Approximate FLOPs + val flops = operations * result.throughput + println(" ${result.name}: ${formatFlops(flops)}") + } + } + + @Test + fun testElementwiseOperationsBaseline() { + println("\n=== ELEMENT-WISE OPERATIONS BASELINE ===") + + // Test moderate sizes for element-wise operations + val testSizes = listOf(1_000, 10_000, 100_000, 1_000_000) + + val results = suite.benchmarkElementwiseOps(testSizes) + + results.forEach { (operation, opResults) -> + println("$operation Operations:") + opResults.forEach { result -> + println(" ${result.prettyPrint()}") + } + println() + } + } + + @Test + fun testDotProductBaseline() { + println("\n=== DOT PRODUCT BASELINE ===") + + val testSizes = listOf(1_000, 10_000, 100_000, 1_000_000) + + val results = suite.benchmarkDotProduct(testSizes) + + results.forEach { result -> + println(result.prettyPrint()) + + // Calculate memory bandwidth estimation + val size = extractVectorSize(result.name) + val bytesProcessed = size * 2 * 4L // Two arrays of floats (4 bytes each) + val bandwidth = bytesProcessed * result.throughput / 1_000_000_000.0 // GB/s + println(" Estimated Memory Bandwidth: ${bandwidth.format(2)} GB/s") + println() + } + } + + @Test + fun testScalarOperationsBaseline() { + println("\n=== SCALAR OPERATIONS BASELINE ===") + + val testSizes = listOf(10_000, 100_000, 1_000_000) + + val results = suite.benchmarkScalarOps(testSizes) + + results.forEach { (operation, opResults) -> + println("$operation Operations:") + opResults.forEach { result -> + println(" ${result.prettyPrint()}") + } + println() + } + } + + @Test + fun testFullBenchmarkSuite() { + println("\n=== COMPREHENSIVE BASELINE BENCHMARK ===") + + val report = suite.runFullBenchmarkSuite() + println(report.prettyPrint()) + + // Performance insights + println("\nPERFORMACE INSIGHTS:") + + // Matrix multiplication scaling analysis + val matmulResults = report.matrixMultiplication.take(4) // First 4 square matrices + if (matmulResults.size >= 2) { + val small = matmulResults[0] + val large = matmulResults[3] + val sizeRatio = 8.0 // 256/32 = 8 + val timeRatio = large.executionTime.mean / small.executionTime.mean + val expectedRatio = sizeRatio * sizeRatio * sizeRatio // O(n³) + + println(" Matrix Multiplication Scaling:") + println(" Size ratio: ${sizeRatio}x") + println(" Time ratio: ${timeRatio.format(2)}x") + println(" Expected O(n³) ratio: ${expectedRatio.format(1)}x") + println(" Efficiency: ${(expectedRatio / timeRatio * 100).format(1)}%") + } + + // Element-wise operations should scale linearly + val addResults = report.elementwiseOperations["Add"] ?: emptyList() + if (addResults.size >= 2) { + val small = addResults[0] + val large = addResults.last() + val smallSize = extractVectorSize(small.name) + val largeSize = extractVectorSize(large.name) + val sizeRatio = largeSize.toDouble() / smallSize + val timeRatio = large.executionTime.mean / small.executionTime.mean + + println(" Element-wise Addition Scaling:") + println(" Size ratio: ${sizeRatio.format(1)}x") + println(" Time ratio: ${timeRatio.format(2)}x") + println(" Linearity: ${(sizeRatio / timeRatio * 100).format(1)}%") + } + } + + @Test + fun testMemoryUsageDemonstration() { + println("\n=== MEMORY USAGE ANALYSIS ===") + + // Note: Current implementation has placeholder memory stats + // This test demonstrates where real memory profiling would go + + val runner = BenchmarkRunner() + + // Large matrix multiplication to observe memory patterns + val result = runner.benchmark( + name = "Large Matrix Memory Test", + warmupRuns = 2, + measurementRuns = 5 + ) { + val backend = CpuBackend() + val a = createLargeMatrix(500, 500) + val b = createLargeMatrix(500, 500) + backend.matmul(a, b) + } + + println(result.prettyPrint()) + println("Note: Memory statistics are placeholders in current multiplatform implementation") + println("Real JVM implementation would show:") + println(" - Heap allocation patterns") + println(" - GC pressure") + println(" - Memory bandwidth utilization") + } + + private fun createLargeMatrix(rows: Int, cols: Int) = + sk.ai.net.core.tensor.backend.CpuTensorFP32.fromArray( + sk.ai.net.core.tensor.Shape(rows, cols), + FloatArray(rows * cols) { (it % 100).toFloat() / 50f } + ) + + private fun extractMatrixSize(name: String): Int { + // Extract size from "MatMul 256x256 * 256x256" format + val regex = """MatMul (\d+)x\d+ \* \d+x\d+""".toRegex() + val match = regex.find(name) + return match?.groupValues?.get(1)?.toInt() ?: 32 + } + + private fun extractVectorSize(name: String): Int { + // Extract size from "DotProduct (1000000 elements)" format + val regex = """\((\d+) elements\)""".toRegex() + val match = regex.find(name) + return match?.groupValues?.get(1)?.toInt() ?: 1000 + } + + private fun formatFlops(flops: Double): String { + return when { + flops >= 1_000_000_000 -> "${(flops / 1_000_000_000).format(2)} GFLOP/s" + flops >= 1_000_000 -> "${(flops / 1_000_000).format(2)} MFLOP/s" + flops >= 1_000 -> "${(flops / 1_000).format(2)} KFLOP/s" + else -> "${flops.format(2)} FLOP/s" + } + } + + private fun Double.format(decimals: Int): String { + val multiplier = when (decimals) { + 0 -> 1.0 + 1 -> 10.0 + 2 -> 100.0 + 3 -> 1000.0 + else -> 1000.0 + } + val rounded = (this * multiplier).toInt() / multiplier + return rounded.toString() + } +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BenchmarkRunner.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BenchmarkRunner.kt new file mode 100644 index 000000000..43a2c52f0 --- /dev/null +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BenchmarkRunner.kt @@ -0,0 +1,156 @@ +package sk.ai.net.core.tensor.benchmark + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.measureTime +import kotlin.time.TimeSource + +/** + * Core benchmark runner for measuring tensor operation performance. + * Multiplatform-compatible implementation using Kotlin stdlib APIs. + */ +class BenchmarkRunner { + + /** + * Runs a comprehensive benchmark of the given operation. + * + * @param name Human-readable name for the benchmark + * @param warmupRuns Number of warmup iterations (default: 10) + * @param measurementRuns Number of measurement iterations (default: 100) + * @param operation The operation to benchmark + * @return Detailed benchmark results including timing statistics + */ + fun benchmark( + name: String, + warmupRuns: Int = 10, + measurementRuns: Int = 100, + operation: () -> T + ): BenchmarkResult { + // Warmup phase to allow JIT compilation + repeat(warmupRuns) { + operation() + } + + val times = mutableListOf() + + // Measurement phase + repeat(measurementRuns) { + val time = measureTime { operation() }.inWholeMicroseconds.toDouble() + times.add(time) + } + + return BenchmarkResult( + name = name, + executionTime = calculateTimeStatistics(times), + memoryUsage = MemoryStatistics(0L, 0L, 0, 0L), // Placeholder for memory stats + throughput = calculateThroughput(times), + metadata = mapOf( + "warmupRuns" to warmupRuns, + "measurementRuns" to measurementRuns, + "timestamp" to TimeSource.Monotonic.markNow().elapsedNow().inWholeMilliseconds + ) + ) + } + + /** + * Advanced warmup with time-based minimum duration + */ + fun warmupWithDuration( + operation: () -> Unit, + minRuns: Int = 10, + minTime: Duration = 5.seconds + ) { + val timeSource = TimeSource.Monotonic + val startMark = timeSource.markNow() + var runs = 0 + + while (runs < minRuns || startMark.elapsedNow() < minTime) { + operation() + runs++ + } + } + + private fun calculateTimeStatistics(times: List): TimeStatistics { + val sorted = times.sorted() + val mean = times.average() + val variance = times.map { (it - mean) * (it - mean) }.average() + val stdDev = kotlin.math.sqrt(variance) + + return TimeStatistics( + mean = mean, + median = sorted[sorted.size / 2], + standardDeviation = stdDev, + min = sorted.first(), + max = sorted.last(), + percentile95 = sorted[(sorted.size * 0.95).toInt()] + ) + } + + private fun calculateThroughput(times: List): Double { + val averageTimeMicros = times.average() + return 1_000_000.0 / averageTimeMicros // operations per second + } +} + +/** + * Comprehensive benchmark result containing timing statistics + */ +data class BenchmarkResult( + val name: String, + val executionTime: TimeStatistics, + val memoryUsage: MemoryStatistics, + val throughput: Double, // operations per second + val metadata: Map +) { + fun prettyPrint(): String { + return buildString { + appendLine("$name:") + appendLine(" Time: ${(executionTime.mean / 1000).format(3)}ms ± ${(executionTime.standardDeviation / 1000).format(3)}ms") + appendLine(" Throughput: ${throughput.format(1)} ops/sec") + appendLine(" Memory: ${formatBytes(memoryUsage.allocatedBytes)}") + appendLine(" Range: ${(executionTime.min / 1000).format(3)}ms - ${(executionTime.max / 1000).format(3)}ms") + } + } + + private fun formatBytes(bytes: Long): String { + return when { + bytes >= 1_000_000 -> "${(bytes / 1_000_000.0).format(1)}MB" + bytes >= 1_000 -> "${(bytes / 1_000.0).format(1)}KB" + else -> "${bytes}B" + } + } + + private fun Double.format(decimals: Int): String { + val multiplier = when (decimals) { + 0 -> 1.0 + 1 -> 10.0 + 2 -> 100.0 + 3 -> 1000.0 + else -> 1000.0 + } + val rounded = (this * multiplier).toInt() / multiplier + return rounded.toString() + } +} + +/** + * Statistical analysis of execution times + */ +data class TimeStatistics( + val mean: Double, // microseconds + val median: Double, // microseconds + val standardDeviation: Double, // microseconds + val min: Double, // microseconds + val max: Double, // microseconds + val percentile95: Double // microseconds +) + +/** + * Memory usage statistics (placeholder for multiplatform compatibility) + */ +data class MemoryStatistics( + val allocatedBytes: Long, + val peakHeapUsage: Long, + val gcCollections: Int, + val gcTime: Long // milliseconds +) \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/TensorBenchmarkSuite.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/TensorBenchmarkSuite.kt new file mode 100644 index 000000000..03ba02b25 --- /dev/null +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/TensorBenchmarkSuite.kt @@ -0,0 +1,347 @@ +package sk.ai.net.core.tensor.benchmark + +import sk.ai.net.core.tensor.* +import sk.ai.net.core.tensor.backend.* + +/** + * Comprehensive benchmark suite for tensor operations. + * Tests performance of matrix multiplication, element-wise operations, and dot products + * across different tensor sizes and data patterns. + */ +class TensorBenchmarkSuite(private val backend: CpuBackend) { + + private val runner = BenchmarkRunner() + + /** + * Benchmark matrix multiplication across different sizes + */ + fun benchmarkMatrixMultiplication( + sizes: List> = defaultMatrixSizes + ): List { + return sizes.map { (rows, inner, cols) -> + val a = createRandomTensor(Shape(rows, inner)) + val b = createRandomTensor(Shape(inner, cols)) + + runner.benchmark( + name = "MatMul ${rows}x${inner} * ${inner}x${cols}", + warmupRuns = 5, + measurementRuns = 50 + ) { + backend.matmul(a, b) + } + } + } + + /** + * Benchmark element-wise operations (add, subtract, multiply, divide) + */ + fun benchmarkElementwiseOps( + sizes: List = defaultVectorSizes + ): Map> { + val operations = mapOf CpuTensorFP32>( + "Add" to { a, b -> with(backend) { a + b } as CpuTensorFP32 }, + "Subtract" to { a, b -> with(backend) { a - b } as CpuTensorFP32 }, + "Multiply" to { a, b -> with(backend) { a * b } as CpuTensorFP32 }, + "Divide" to { a, b -> with(backend) { a / b } as CpuTensorFP32 } + ) + + return operations.mapValues { (opName, operation) -> + sizes.map { size -> + val a = createRandomTensor(Shape(size)) + val b = createRandomTensor(Shape(size)) + + runner.benchmark( + name = "$opName (${size} elements)", + warmupRuns = 10, + measurementRuns = 100 + ) { + operation(a, b) + } + } + } + } + + /** + * Benchmark dot product operations + */ + fun benchmarkDotProduct( + sizes: List = defaultVectorSizes + ): List { + return sizes.map { size -> + val a = createRandomTensor(Shape(size)) + val b = createRandomTensor(Shape(size)) + + runner.benchmark( + name = "DotProduct (${size} elements)", + warmupRuns = 10, + measurementRuns = 100 + ) { + backend.dot(a, b) + } + } + } + + /** + * Benchmark scalar operations + */ + fun benchmarkScalarOps( + sizes: List = defaultVectorSizes + ): Map> { + val scalarOps = mapOf CpuTensorFP32>( + "Scale" to { tensor -> backend.scale(tensor, 2.5) as CpuTensorFP32 }, + "Add Scalar" to { tensor -> with(backend) { tensor + 3.14f } as CpuTensorFP32 }, + "Multiply Scalar" to { tensor -> with(backend) { tensor * 2.0f } as CpuTensorFP32 } + ) + + return scalarOps.mapValues { (opName, operation) -> + sizes.map { size -> + val tensor = createRandomTensor(Shape(size)) + + runner.benchmark( + name = "$opName (${size} elements)", + warmupRuns = 10, + measurementRuns = 100 + ) { + operation(tensor) + } + } + } + } + + /** + * Run comprehensive benchmark suite covering all operations + */ + fun runFullBenchmarkSuite(): BenchmarkReport { + println("Starting comprehensive tensor benchmark suite...") + + val matmulResults = benchmarkMatrixMultiplication() + val elementwiseResults = benchmarkElementwiseOps() + val dotProductResults = benchmarkDotProduct() + val scalarResults = benchmarkScalarOps() + + return BenchmarkReport( + backendName = backend.name, + matrixMultiplication = matmulResults, + elementwiseOperations = elementwiseResults, + dotProduct = dotProductResults, + scalarOperations = scalarResults, + summary = generateSummary(matmulResults, elementwiseResults, dotProductResults, scalarResults) + ) + } + + /** + * Compare performance between two backends (e.g., scalar vs SIMD) + */ + fun compareBackends( + otherBackend: CpuBackend, + otherBackendName: String + ): ComparisonReport { + println("Comparing performance between ${backend.name} and $otherBackendName...") + + val thisResults = runFullBenchmarkSuite() + val otherSuite = TensorBenchmarkSuite(otherBackend) + val otherResults = otherSuite.runFullBenchmarkSuite() + + return ComparisonReport( + baselineBackend = backend.name, + baselineResults = thisResults, + comparisonBackend = otherBackendName, + comparisonResults = otherResults, + speedupAnalysis = calculateSpeedups(thisResults, otherResults) + ) + } + + private fun createRandomTensor(shape: Shape): CpuTensorFP32 { + val size = shape.volume + val data = FloatArray(size) { (it % 100 + 1).toFloat() / 50f } // Values between 0.02 and 2.0 + return CpuTensorFP32.fromArray(shape, data) + } + + private fun generateSummary( + matmul: List, + elementwise: Map>, + dotProduct: List, + scalar: Map> + ): BenchmarkSummary { + val allResults = buildList { + addAll(matmul) + elementwise.values.forEach { addAll(it) } + addAll(dotProduct) + scalar.values.forEach { addAll(it) } + } + + return BenchmarkSummary( + totalOperations = allResults.size, + averageThroughput = allResults.map { it.throughput }.average(), + fastestOperation = allResults.maxByOrNull { it.throughput }?.name ?: "N/A", + slowestOperation = allResults.minByOrNull { it.throughput }?.name ?: "N/A" + ) + } + + private fun calculateSpeedups( + baseline: BenchmarkReport, + comparison: BenchmarkReport + ): SpeedupAnalysis { + val speedups = mutableMapOf() + + // Calculate speedups for matrix multiplication + baseline.matrixMultiplication.zip(comparison.matrixMultiplication) { base, comp -> + speedups[base.name] = base.executionTime.mean / comp.executionTime.mean + } + + // Calculate speedups for element-wise operations + baseline.elementwiseOperations.forEach { (opType, baseResults) -> + val compResults = comparison.elementwiseOperations[opType] ?: emptyList() + baseResults.zip(compResults) { base, comp -> + speedups[base.name] = base.executionTime.mean / comp.executionTime.mean + } + } + + val speedupValues = speedups.values.toList() + + return SpeedupAnalysis( + operationSpeedups = speedups, + averageSpeedup = speedupValues.average(), + bestCaseSpeedup = speedupValues.maxOrNull() ?: 1.0, + worstCaseSpeedup = speedupValues.minOrNull() ?: 1.0, + speedupByTensorSize = speedups // Simplified - could be grouped by size + ) + } + + companion object { + // Default benchmark sizes + val defaultMatrixSizes = listOf( + Triple(32, 32, 32), // Small + Triple(64, 64, 64), + Triple(128, 128, 128), // Medium + Triple(256, 256, 256), + Triple(512, 512, 512), // Large + Triple(1024, 1024, 1024), + Triple(100, 500, 200), // Rectangular + Triple(1000, 10, 1000), // Thin + Triple(10, 1000, 10) // Wide + ) + + val defaultVectorSizes = listOf( + 100, 1_000, 10_000, 100_000, 1_000_000 + ) + } +} + +/** + * Comprehensive benchmark report containing all operation results + */ +data class BenchmarkReport( + val backendName: String, + val matrixMultiplication: List, + val elementwiseOperations: Map>, + val dotProduct: List, + val scalarOperations: Map>, + val summary: BenchmarkSummary +) { + fun prettyPrint(): String { + return buildString { + appendLine("=" .repeat(50)) + appendLine("TENSOR BENCHMARK REPORT - $backendName") + appendLine("=" .repeat(50)) + appendLine() + + appendLine("MATRIX MULTIPLICATION:") + matrixMultiplication.forEach { result -> + appendLine(result.prettyPrint()) + } + appendLine() + + appendLine("ELEMENT-WISE OPERATIONS:") + elementwiseOperations.forEach { (opType, results) -> + appendLine(" $opType:") + results.forEach { result -> + appendLine(" ${result.prettyPrint().prependIndent(" ")}") + } + } + appendLine() + + appendLine("SUMMARY:") + appendLine(" Total Operations: ${summary.totalOperations}") + appendLine(" Average Throughput: ${summary.averageThroughput.format(1)} ops/sec") + appendLine(" Fastest: ${summary.fastestOperation}") + appendLine(" Slowest: ${summary.slowestOperation}") + } + } + + private fun Double.format(decimals: Int): String { + val multiplier = when (decimals) { + 0 -> 1.0 + 1 -> 10.0 + 2 -> 100.0 + 3 -> 1000.0 + else -> 1000.0 + } + val rounded = (this * multiplier).toInt() / multiplier + return rounded.toString() + } +} + +/** + * Summary statistics for a benchmark run + */ +data class BenchmarkSummary( + val totalOperations: Int, + val averageThroughput: Double, + val fastestOperation: String, + val slowestOperation: String +) + +/** + * Backend comparison report showing speedup analysis + */ +data class ComparisonReport( + val baselineBackend: String, + val baselineResults: BenchmarkReport, + val comparisonBackend: String, + val comparisonResults: BenchmarkReport, + val speedupAnalysis: SpeedupAnalysis +) { + fun prettyPrint(): String { + return buildString { + appendLine("=" .repeat(60)) + appendLine("BACKEND COMPARISON: $baselineBackend vs $comparisonBackend") + appendLine("=" .repeat(60)) + appendLine() + + appendLine("SPEEDUP ANALYSIS:") + appendLine(" Average Speedup: ${speedupAnalysis.averageSpeedup.format(2)}x") + appendLine(" Best Case: ${speedupAnalysis.bestCaseSpeedup.format(2)}x") + appendLine(" Worst Case: ${speedupAnalysis.worstCaseSpeedup.format(2)}x") + appendLine() + + appendLine("DETAILED SPEEDUPS:") + speedupAnalysis.operationSpeedups.forEach { (operation, speedup) -> + appendLine(" $operation: ${speedup.format(2)}x") + } + } + } + + private fun Double.format(decimals: Int): String { + val multiplier = when (decimals) { + 0 -> 1.0 + 1 -> 10.0 + 2 -> 100.0 + 3 -> 1000.0 + else -> 1000.0 + } + val rounded = (this * multiplier).toInt() / multiplier + return rounded.toString() + } +} + +/** + * Speedup analysis comparing two backends + */ +data class SpeedupAnalysis( + val operationSpeedups: Map, + val averageSpeedup: Double, + val bestCaseSpeedup: Double, + val worstCaseSpeedup: Double, + val speedupByTensorSize: Map +) \ No newline at end of file From 8d5830dc0ed195e48230049590123b7361658bd9 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 08:20:15 +0200 Subject: [PATCH 05/16] Add initial version of arc42 docs Related-To: #101 --- docs/antora.yml | 6 + docs/modules/ROOT/nav.adoc | 14 + docs/modules/ROOT/pages/index.adoc | 0 docs/modules/arc42/nav.adoc | 3 + .../pages/01-introduction-and-goals.adoc | 81 ++++ .../pages/02-architecture-constraints.adoc | 75 ++++ .../arc42/pages/03-context-and-scope.adoc | 160 ++++++++ .../arc42/pages/04-solution-strategy.adoc | 178 +++++++++ .../arc42/pages/05-building-block-view.adoc | 225 +++++++++++ docs/modules/arc42/pages/06-runtime-view.adoc | 278 +++++++++++++ .../arc42/pages/07-deployment-view.adoc | 254 ++++++++++++ .../arc42/pages/08-crosscutting-concepts.adoc | 277 +++++++++++++ .../pages/09-architecture-decisions.adoc | 318 +++++++++++++++ .../arc42/pages/10-quality-requirements.adoc | 370 ++++++++++++++++++ .../pages/11-risks-and-technical-debt.adoc | 337 ++++++++++++++++ docs/modules/arc42/pages/12-glossary.adoc | 166 ++++++++ 16 files changed, 2742 insertions(+) create mode 100644 docs/antora.yml create mode 100644 docs/modules/ROOT/nav.adoc create mode 100644 docs/modules/ROOT/pages/index.adoc create mode 100644 docs/modules/arc42/nav.adoc create mode 100644 docs/modules/arc42/pages/01-introduction-and-goals.adoc create mode 100644 docs/modules/arc42/pages/02-architecture-constraints.adoc create mode 100644 docs/modules/arc42/pages/03-context-and-scope.adoc create mode 100644 docs/modules/arc42/pages/04-solution-strategy.adoc create mode 100644 docs/modules/arc42/pages/05-building-block-view.adoc create mode 100644 docs/modules/arc42/pages/06-runtime-view.adoc create mode 100644 docs/modules/arc42/pages/07-deployment-view.adoc create mode 100644 docs/modules/arc42/pages/08-crosscutting-concepts.adoc create mode 100644 docs/modules/arc42/pages/09-architecture-decisions.adoc create mode 100644 docs/modules/arc42/pages/10-quality-requirements.adoc create mode 100644 docs/modules/arc42/pages/11-risks-and-technical-debt.adoc create mode 100644 docs/modules/arc42/pages/12-glossary.adoc diff --git a/docs/antora.yml b/docs/antora.yml new file mode 100644 index 000000000..e843664f9 --- /dev/null +++ b/docs/antora.yml @@ -0,0 +1,6 @@ +name: skainet +title: Skainet Deep Learning Framework +version: '1.0' +nav: +- modules/ROOT/nav.adoc +- modules/arc42/nav.adoc \ No newline at end of file diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc new file mode 100644 index 000000000..27a1b385e --- /dev/null +++ b/docs/modules/ROOT/nav.adoc @@ -0,0 +1,14 @@ += SKaiNET Architecture Documentation + +* xref:01-introduction-and-goals.adoc[Introduction and Goals] +* xref:02-architecture-constraints.adoc[Architecture Constraints] +* xref:03-context-and-scope.adoc[Context and Scope] +* xref:04-solution-strategy.adoc[Solution Strategy] +* xref:05-building-block-view.adoc[Building Block View] +* xref:06-runtime-view.adoc[Runtime View] +* xref:07-deployment-view.adoc[Deployment View] +* xref:08-crosscutting-concepts.adoc[Crosscutting Concepts] +* xref:09-architecture-decisions.adoc[Architecture Decisions] +* xref:10-quality-requirements.adoc[Quality Requirements] +* xref:11-risks-and-technical-debt.adoc[Risks and Technical Debt] +* xref:12-glossary.adoc[Glossary] \ No newline at end of file diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc new file mode 100644 index 000000000..e69de29bb diff --git a/docs/modules/arc42/nav.adoc b/docs/modules/arc42/nav.adoc new file mode 100644 index 000000000..32b6d9152 --- /dev/null +++ b/docs/modules/arc42/nav.adoc @@ -0,0 +1,3 @@ += SKaiNET + +* xref:index.adoc[Introduction and Goals] diff --git a/docs/modules/arc42/pages/01-introduction-and-goals.adoc b/docs/modules/arc42/pages/01-introduction-and-goals.adoc new file mode 100644 index 000000000..a8e02bb18 --- /dev/null +++ b/docs/modules/arc42/pages/01-introduction-and-goals.adoc @@ -0,0 +1,81 @@ += Introduction and Goals + +[role="arc42help"] +**** +Describes the relevant requirements and the driving forces that software architects and development team must consider. +These include + +* underlying business goals, +* essential features, +* essential functional requirements, +* quality goals for the architecture and +* relevant stakeholders and their expectations +**** + +== Requirements Overview + +=== What is SKaiNET? + +SKaiNET is an open-source deep learning framework written in Kotlin, designed with developers in mind to enable the creation of modern AI-powered applications with ease. The framework provides a comprehensive tensor computation system with multiple backend support for different hardware platforms. + +=== Key Features + +* *Multi-dimensional Tensor Operations*: Complete support for tensor mathematics including matrix multiplication, element-wise operations, and broadcasting +* *Pluggable Backend Architecture*: Abstracted computation backends allowing CPU, GPU, and other hardware-specific implementations +* *Type-Safe API*: Kotlin-based type-safe tensor operations with compile-time shape and type checking +* *Performance Optimizations*: Built-in performance measurement and benchmarking framework for optimization analysis +* *Multiplatform Support*: Kotlin Multiplatform compatibility for cross-platform deployment + +== Quality Goals + +[options="header",cols="1,2,2"] +|=== +| Priority | Quality Goal | Motivation + +| 1 +| Performance +| Enable high-performance tensor computations competitive with established frameworks like TensorFlow and PyTorch + +| 2 +| Type Safety +| Leverage Kotlin's type system to catch tensor shape and type mismatches at compile time + +| 3 +| Extensibility +| Support pluggable backends for different hardware platforms (CPU, GPU, TPU, etc.) + +| 4 +| Developer Experience +| Provide intuitive, Kotlin-idiomatic APIs that are easy to learn and use + +| 5 +| Cross-platform Compatibility +| Support deployment across JVM, Android, Native, and JavaScript platforms +|=== + +== Stakeholders + +[options="header",cols="1,2,2"] +|=== +| Role/Name | Contact | Expectations + +| AI/ML Developers +| Primary users +| Easy-to-use, performant tensor operations with good documentation + +| Backend Engineers +| System integrators +| Stable, well-tested APIs for building AI-powered applications + +| Platform Engineers +| Infrastructure teams +| Efficient resource utilization and good observability + +| Framework Contributors +| Open source community +| Clear architecture, good test coverage, contribution guidelines + +| Research Scientists +| Academic/industry researchers +| Flexibility for experimental algorithms and custom operations +|=== \ No newline at end of file diff --git a/docs/modules/arc42/pages/02-architecture-constraints.adoc b/docs/modules/arc42/pages/02-architecture-constraints.adoc new file mode 100644 index 000000000..224438085 --- /dev/null +++ b/docs/modules/arc42/pages/02-architecture-constraints.adoc @@ -0,0 +1,75 @@ += Architecture Constraints + +[role="arc42help"] +**** +Any requirement that constrains software architects in their freedom of design and implementation decisions or decision about the development process. These constraints sometimes go beyond individual systems and are valid for whole organizations and companies. +**** + +== Technical Constraints + +[options="header",cols="1,3,2"] +|=== +| Constraint | Description | Background + +| Kotlin Multiplatform +| Framework must be implemented in Kotlin and support multiplatform deployment (JVM, Native, JS) +| Enables cross-platform compatibility and leverages Kotlin's type safety features + +| Memory Management +| Must work efficiently within JVM garbage collection and native memory constraints +| Critical for performance in tensor operations with large data arrays + +| No External Dependencies +| Core tensor operations should minimize external dependencies +| Reduces complexity, improves security, and enables easier deployment + +| Backward Compatibility +| API changes must maintain backward compatibility following semantic versioning +| Ensures stable integration for dependent applications and libraries +|=== + +== Organizational Constraints + +[options="header",cols="1,3,2"] +|=== +| Constraint | Description | Background + +| Open Source License +| Must use permissive open source license (likely Apache 2.0 or MIT) +| Enables broad adoption in commercial and academic contexts + +| GitFlow Development +| Development follows GitFlow branching model with feature branches +| Ensures structured release management and code quality + +| Test-Driven Development +| All features must have comprehensive test coverage including benchmarks +| Maintains code quality and performance regression detection + +| Documentation Standards +| All public APIs must be documented with KDoc and architecture with Arc42 +| Ensures maintainability and ease of adoption +|=== + +== Conventions + +[options="header",cols="1,3,2"] +|=== +| Convention | Description | Rationale + +| Semantic Versioning +| Follows SemVer (MAJOR.MINOR.PATCH) for all releases +| Provides clear expectations about API changes and compatibility + +| Kotlin Coding Standards +| Adheres to official Kotlin coding conventions and style guide +| Ensures consistent, readable codebase across all modules + +| Performance Benchmarking +| All core operations must include performance benchmarks +| Enables performance regression detection and optimization tracking + +| Type Safety First +| Leverages Kotlin's type system for compile-time error detection +| Reduces runtime errors and improves developer experience +|=== \ No newline at end of file diff --git a/docs/modules/arc42/pages/03-context-and-scope.adoc b/docs/modules/arc42/pages/03-context-and-scope.adoc new file mode 100644 index 000000000..6e698afc7 --- /dev/null +++ b/docs/modules/arc42/pages/03-context-and-scope.adoc @@ -0,0 +1,160 @@ += Context and Scope + +[role="arc42help"] +**** +System scope and context - as the name suggests - delimits your system (i.e. your scope) from all its communication partners (neighboring systems and users, i.e. the context of your system). It thereby specifies the external interfaces. +**** + +== Business Context + +[plantuml, business-context, svg] +.... +!theme plain + +package "AI/ML Application Ecosystem" { + [Mobile Apps] as mobile + [Desktop Applications] as desktop + [Web Applications] as web + [Backend Services] as backend + [Research Tools] as research +} + +package "SKaiNET Framework" { + [Tensor Operations API] as api + [Compute Backends] as backends + [Performance Framework] as perf +} + +package "Hardware Platforms" { + [CPU] as cpu + [GPU] as gpu + [Mobile Hardware] as mobile_hw +} + +package "Development Ecosystem" { + [Kotlin Multiplatform] as kmp + [JVM Runtime] as jvm + [Native Runtime] as native + [JavaScript Runtime] as js +} + +mobile --> api : "Tensor computations" +desktop --> api : "AI model inference" +web --> api : "ML operations" +backend --> api : "Batch processing" +research --> api : "Experimental algorithms" + +api --> backends : "Delegates operations" +backends --> cpu : "CPU computations" +backends --> gpu : "GPU acceleration" +backends --> mobile_hw : "Mobile optimization" + +api --> perf : "Performance monitoring" +backends --> kmp : "Multiplatform support" +kmp --> jvm : "JVM deployment" +kmp --> native : "Native compilation" +kmp --> js : "Browser/Node.js" +.... + +=== External Interfaces + +[options="header",cols="1,2,3,2"] +|=== +| Partner | Interface | Purpose | Technology + +| AI/ML Applications +| Tensor API +| Primary interface for tensor operations, model building, and inference +| Kotlin API, strongly typed + +| Hardware Platforms +| Compute Backend SPI +| Abstraction layer for different hardware acceleration +| Plugin architecture, native bindings + +| Development Tools +| Build Integration +| Framework distribution and dependency management +| Gradle, Maven, package managers + +| Monitoring Systems +| Performance Metrics +| Runtime performance data and benchmarking results +| Structured logging, metrics APIs +|=== + +== Technical Context + +[plantuml, technical-context, svg] +.... +!theme plain + +package "SKaiNET Core" { + component "SKaiNET-tensors-api" as api { + interface "Tensor" as tensor_if + interface "ComputeBackend" as backend_if + interface "TensorOps" as ops_if + } + + component "SKaiNET-tensors" as impl { + class "CpuBackend" as cpu_backend + class "CpuTensorFP32" as cpu_tensor + class "BenchmarkRunner" as benchmark + } +} + +package "Platform Runtime" { + component "Kotlin/JVM" as kjvm + component "Kotlin/Native" as knative + component "Kotlin/JS" as kjs +} + +package "System Libraries" { + component "JVM Math Libraries" as jvm_math + component "Native BLAS" as native_blas + component "WASM Runtime" as wasm +} + +api ..> impl : implements +impl --> kjvm : "JVM bytecode" +impl --> knative : "Native binary" +impl --> kjs : "JavaScript" + +kjvm --> jvm_math : "Java Math, Arrays" +knative --> native_blas : "CBLAS, LAPACK" +kjs --> wasm : "WebAssembly modules" + +cpu_backend ..|> backend_if : implements +cpu_tensor ..|> tensor_if : implements +.... + +=== Technology Stack + +[options="header",cols="1,2,3"] +|=== +| Layer | Technology | Purpose + +| Language +| Kotlin Multiplatform +| Cross-platform development with type safety + +| Runtime Targets +| JVM, Native, JavaScript +| Deployment across different environments + +| Build System +| Gradle with Kotlin DSL +| Dependency management and multiplatform builds + +| Testing +| Kotlin Test, JUnit +| Unit testing and integration testing + +| Performance +| Custom benchmarking framework +| Performance measurement and regression detection + +| Documentation +| KDoc, Antora, Arc42 +| API documentation and architecture documentation +|=== \ No newline at end of file diff --git a/docs/modules/arc42/pages/04-solution-strategy.adoc b/docs/modules/arc42/pages/04-solution-strategy.adoc new file mode 100644 index 000000000..4a12b64a8 --- /dev/null +++ b/docs/modules/arc42/pages/04-solution-strategy.adoc @@ -0,0 +1,178 @@ += Solution Strategy + +[role="arc42help"] +**** +A short summary and explanation of the fundamental decisions and solution strategies, that shape the system's architecture. These include + +* technology decisions +* decisions about the top-level decomposition of the system +* decisions on how to achieve key quality goals +* relevant organizational decisions +**** + +== Technology Decisions + +=== Kotlin Multiplatform as Foundation + +*Decision*: Use Kotlin Multiplatform for the entire framework implementation. + +*Rationale*: +* Enables true cross-platform deployment (JVM, Native, JavaScript) +* Provides excellent type safety and null safety features +* Allows sharing business logic while maintaining platform-specific optimizations +* Growing ecosystem with strong tooling support + +*Consequences*: +* Framework can target multiple platforms with single codebase +* Leverages Kotlin's advanced type system for tensor shape validation +* Requires understanding of multiplatform constraints and limitations + +=== Backend Abstraction Pattern + +*Decision*: Implement pluggable backend architecture with `ComputeBackend` interface. + +*Rationale*: +* Allows optimization for different hardware platforms (CPU, GPU, TPU) +* Enables easy integration of specialized libraries (BLAS, CUDA, etc.) +* Supports future hardware without API changes +* Facilitates performance comparison between implementations + +*Consequences*: +* Clean separation between API and implementation +* Potential runtime overhead from abstraction layer +* Complex testing matrix across different backends + +== Architectural Patterns + +=== Layered Architecture + +[plantuml, layered-architecture, svg] +.... +!theme plain + +package "Application Layer" { + [AI/ML Applications] as apps +} + +package "API Layer" { + [Tensor Operations API] as api + [Type System] as types +} + +package "Backend Abstraction Layer" { + [ComputeBackend Interface] as backend_if + [Backend Registry] as registry +} + +package "Implementation Layer" { + [CpuBackend] as cpu + [GpuBackend] as gpu + [OptimizedBackend] as opt +} + +package "Platform Layer" { + [Kotlin/JVM] as jvm + [Kotlin/Native] as native + [Kotlin/JS] as js +} + +apps --> api +api --> types +api --> backend_if +backend_if --> registry +registry --> cpu +registry --> gpu +registry --> opt +cpu --> jvm +cpu --> native +gpu --> native +opt --> jvm +.... + +*Benefits*: +* Clear separation of concerns +* Easy to test individual layers +* Backend implementations can be developed independently +* API remains stable while implementations evolve + +=== Strategy Pattern for Backends + +*Decision*: Use Strategy pattern for compute backend selection and execution. + +*Implementation*: +* `ComputeBackend` interface defines operations contract +* Runtime backend selection based on availability and performance +* Each backend optimized for specific hardware characteristics + +== Quality Goal Achievement Strategies + +=== Performance Strategy + +*Approach*: +* Comprehensive benchmarking framework integrated into the build process +* Performance regression detection in CI/CD pipeline +* Hardware-specific optimizations through pluggable backends +* Memory-efficient tensor data structures + +*Implementation*: +* `BenchmarkRunner` class for consistent performance measurement +* Automated performance testing in `BaselinePerformanceTest` +* Memory usage tracking and optimization recommendations + +=== Type Safety Strategy + +*Approach*: +* Leverage Kotlin's type system for compile-time error detection +* Generic type parameters for data types and value types +* Shape validation at tensor creation time +* Operator overloading for intuitive mathematical syntax + +*Implementation*: +```kotlin +interface Tensor : TensorData, TensorOps> +``` + +=== Extensibility Strategy + +*Approach*: +* Plugin architecture for compute backends +* Service Provider Interface (SPI) pattern +* Clean separation between API and implementation +* Documentation-driven development for extension points + +== Organizational Decisions + +=== Development Process + +*Decision*: Adopt GitFlow with comprehensive testing requirements. + +*Implementation*: +* Feature branches for all development +* Mandatory code review process +* Automated testing including performance benchmarks +* Documentation updates required for all API changes + +=== Open Source Strategy + +*Decision*: Full open source development with permissive licensing. + +*Benefits*: +* Community contributions and feedback +* Transparency in implementation decisions +* Academic and commercial adoption +* Integration with broader Kotlin ecosystem + +=== Modular Architecture + +*Decision*: Separate API and implementation into distinct modules. + +*Structure*: +* `SKaiNET-tensors-api`: Pure interfaces and contracts +* `SKaiNET-tensors`: Reference implementations +* Future modules: `SKaiNET-gpu`, `SKaiNET-distributed`, etc. + +*Benefits*: +* Clear API boundaries +* Independent evolution of implementations +* Easier testing and mocking +* Reduced compilation dependencies \ No newline at end of file diff --git a/docs/modules/arc42/pages/05-building-block-view.adoc b/docs/modules/arc42/pages/05-building-block-view.adoc new file mode 100644 index 000000000..b2d8197ff --- /dev/null +++ b/docs/modules/arc42/pages/05-building-block-view.adoc @@ -0,0 +1,225 @@ += Building Block View + +[role="arc42help"] +**** +The building block view shows the static decomposition of the system into building blocks (modules, components, subsystems, classes, interfaces, packages, libraries, frameworks, layers, partitions, tiers, functions, macros, operations, datas structures, ...) as well as their dependencies (relationships, associations, ...) +**** + +== Level 1: System Overview + +[plantuml, level1-overview, svg] +.... +!theme plain + +package "SKaiNET Framework" { + [SKaiNET-tensors-api] as api + [SKaiNET-tensors] as impl + [Performance Framework] as perf +} + +package "External Systems" { + [AI/ML Applications] as apps + [Hardware Platforms] as hardware + [Kotlin Multiplatform] as kmp +} + +apps --> api : "uses" +api --> impl : "implemented by" +impl --> perf : "includes" +impl --> hardware : "executes on" +api --> kmp : "built with" +impl --> kmp : "built with" +.... + +=== System Decomposition + +[options="header",cols="1,3,2"] +|=== +| Component | Responsibility | Technology + +| SKaiNET-tensors-api +| Defines public interfaces for tensor operations, data types, and compute backends +| Kotlin Multiplatform interfaces + +| SKaiNET-tensors +| Reference implementation with CPU backend and performance benchmarking +| Kotlin Multiplatform implementation + +| Performance Framework +| Benchmarking, measurement, and performance analysis tools +| Integrated testing framework +|=== + +== Level 2: API Module Decomposition + +[plantuml, level2-api, svg] +.... +!theme plain + +package "SKaiNET-tensors-api" { + + package "Core Types" { + interface "DType" as dtype + interface "Shape" as shape + interface "TensorData" as data + } + + package "Operations" { + interface "TensorOps" as ops + interface "Tensor" as tensor + } + + package "Backend" { + interface "ComputeBackend" as backend + } +} + +tensor --|> data : "extends" +tensor --|> ops : "extends" +backend --|> ops : "extends" +tensor --> dtype : "uses" +tensor --> shape : "uses" +data --> shape : "uses" +.... + +=== Core Components Description + +==== DType (Data Type) +*Purpose*: Sealed interface defining supported numerical data types. + +*Responsibilities*: +* Type-safe representation of tensor element types +* Compile-time type checking for operations +* Platform-specific optimizations + +*Key Types*: +* `FP32`: 32-bit floating point +* `FP64`: 64-bit floating point +* `INT32`: 32-bit integer +* `INT64`: 64-bit integer + +==== Shape +*Purpose*: Immutable representation of tensor dimensions. + +*Responsibilities*: +* Multi-dimensional array shape definition +* Dimension validation and computation +* Broadcasting compatibility checks + +*Key Operations*: +* Dimension access and validation +* Total element count calculation +* Shape compatibility verification + +==== Tensor +*Purpose*: Primary interface for multi-dimensional arrays. + +*Type Parameters*: +* `T : DType`: Compile-time data type +* `V`: Runtime value type (Float, Double, Int, Long) + +*Responsibilities*: +* Element access with multi-dimensional indexing +* Integration with TensorData for data management +* Integration with TensorOps for operations + +== Level 3: Implementation Module Decomposition + +[plantuml, level3-impl, svg] +.... +!theme plain + +package "SKaiNET-tensors" { + + package "CPU Backend" { + class "CpuBackend" as cpu_backend + class "CpuTensorFP32" as cpu_tensor + } + + package "Benchmarking" { + class "BenchmarkRunner" as runner + class "TensorBenchmarkSuite" as suite + class "BaselinePerformanceTest" as baseline + } + + package "Test Support" { + class "TestUtils" as utils + } +} + +package "API Dependencies" { + interface "ComputeBackend" as backend_if + interface "Tensor" as tensor_if +} + +cpu_backend ..|> backend_if : implements +cpu_tensor ..|> tensor_if : implements +cpu_backend --> cpu_tensor : creates +suite --> runner : uses +baseline --> suite : uses +baseline --> utils : uses +.... + +=== Implementation Components + +==== CpuBackend +*Purpose*: CPU-optimized implementation of ComputeBackend. + +*Key Features*: +* Matrix multiplication with O(n³) complexity +* Element-wise operations (add, subtract, multiply, divide) +* Scalar operations with broadcasting +* Memory-efficient array operations + +*Performance Characteristics*: +* Optimized for single-threaded CPU execution +* Uses native array operations where possible +* Baseline implementation for performance comparison + +==== CpuTensorFP32 +*Purpose*: Concrete tensor implementation for 32-bit floating point data. + +*Internal Structure*: +```kotlin +class CpuTensorFP32( + override val shape: Shape, + internal val data: FloatArray +) : Tensor +``` + +*Key Methods*: +* Factory methods: `zeros()`, `ones()`, `full()`, `fromArray()` +* Element access: `get(vararg indices: Int)` +* Operator overloading for mathematical operations + +==== Performance Framework +*Purpose*: Comprehensive benchmarking and performance analysis. + +*Components*: +* `BenchmarkRunner`: Core benchmarking execution engine +* `TensorBenchmarkSuite`: Predefined benchmark scenarios +* `BaselinePerformanceTest`: Automated performance regression testing + +*Metrics Collected*: +* Execution time statistics (mean, std dev, percentiles) +* Memory usage patterns +* Performance scaling analysis +* Operation throughput measurements + +== Dependencies and Relationships + +=== Internal Dependencies +* API module defines contracts, implementation module fulfills them +* Performance framework depends on both API and implementation +* Test utilities support both unit and performance testing + +=== External Dependencies +* Kotlin Standard Library (collections, math functions) +* Kotlin Test framework for testing infrastructure +* Platform-specific optimizations (JVM arrays, native BLAS) + +=== Design Principles Applied +* *Separation of Concerns*: Clear boundary between API and implementation +* *Open/Closed Principle*: Easy to add new backends without API changes +* *Dependency Inversion*: High-level modules depend on abstractions +* *Single Responsibility*: Each component has focused, well-defined purpose \ No newline at end of file diff --git a/docs/modules/arc42/pages/06-runtime-view.adoc b/docs/modules/arc42/pages/06-runtime-view.adoc new file mode 100644 index 000000000..c6a6671f9 --- /dev/null +++ b/docs/modules/arc42/pages/06-runtime-view.adoc @@ -0,0 +1,278 @@ += Runtime View + +[role="arc42help"] +**** +The runtime view describes concrete behavior and interactions of the system's building blocks in form of scenarios for the following purposes: + +* important use cases or features: how do building blocks execute them? +* interactions at critical external interfaces: how do building blocks cooperate with users and neighboring systems? +* operation and administration: launch, start-up, stop +* error and exception scenarios +**** + +== Tensor Creation and Initialization + +=== Scenario: Creating a Tensor from Data + +[plantuml, tensor-creation, svg] +.... +!theme plain + +actor "Application" as app +participant "CpuTensorFP32" as tensor +participant "Shape" as shape +participant "FloatArray" as data + +app -> tensor : fromArray(shape, data) +activate tensor + +tensor -> shape : validate dimensions +activate shape +shape --> tensor : validation result +deactivate shape + +alt validation successful + tensor -> tensor : create instance + tensor -> data : copy/reference data + tensor --> app : Tensor +else validation failed + tensor --> app : throw IllegalArgumentException +end + +deactivate tensor +.... + +*Description*: Application creates a tensor by providing shape and data array. + +*Key Steps*: +1. Application calls factory method with shape and data +2. Shape validation ensures data array size matches dimensions +3. Tensor instance created with validated parameters +4. Reference or copy of data array stored internally + +*Error Handling*: +* Invalid shape dimensions → `IllegalArgumentException` +* Data array size mismatch → `IllegalArgumentException` +* Null parameters → `NullPointerException` + +== Backend Operations + +=== Scenario: Matrix Multiplication + +[plantuml, matrix-multiplication, svg] +.... +!theme plain + +actor "Application" as app +participant "CpuBackend" as backend +participant "Tensor A" as tensorA +participant "Tensor B" as tensorB +participant "Result Tensor" as result + +app -> backend : matmul(tensorA, tensorB) +activate backend + +backend -> tensorA : validate shape +backend -> tensorB : validate shape + +alt shapes compatible + backend -> backend : calculate result shape + backend -> result : create result tensor + activate result + + loop for each row i + loop for each column j + backend -> tensorA : get row i + backend -> tensorB : get column j + backend -> backend : dot product + backend -> result : set(i, j, value) + end + end + + backend --> app : result tensor + deactivate result +else incompatible shapes + backend --> app : throw IllegalArgumentException +end + +deactivate backend +.... + +*Description*: Matrix multiplication between two tensors using CPU backend. + +*Performance Characteristics*: +* Time Complexity: O(n³) for square matrices +* Space Complexity: O(n²) for result storage +* Memory Access: Sequential for optimal cache utilization + +*Validation Rules*: +* Matrix A columns must equal Matrix B rows +* Both tensors must be 2-dimensional +* Compatible data types required + +== Performance Benchmarking + +=== Scenario: Benchmark Execution + +[plantuml, benchmark-execution, svg] +.... +!theme plain + +actor "Test Runner" as test +participant "BenchmarkRunner" as runner +participant "Operation" as op +participant "Statistics" as stats + +test -> runner : benchmark(name, warmup, measurement, operation) +activate runner + +loop warmup runs + runner -> op : execute() + runner -> runner : discard result +end + +loop measurement runs + runner -> runner : start timer + runner -> op : execute() + runner -> runner : stop timer + runner -> runner : record time +end + +runner -> stats : calculate statistics +activate stats +stats -> stats : compute mean, std dev +stats -> stats : detect outliers +stats --> runner : performance metrics +deactivate stats + +runner --> test : BenchmarkResult +deactivate runner +.... + +*Description*: Performance benchmarking framework measuring operation execution time. + +*Measurement Process*: +1. Warmup phase to stabilize JVM performance +2. Measurement phase with precise timing +3. Statistical analysis of results +4. Outlier detection and removal + +*Metrics Collected*: +* Execution time (mean, standard deviation, percentiles) +* Memory usage patterns +* Performance scaling characteristics +* Throughput measurements + +== Error Handling Scenarios + +=== Scenario: Invalid Tensor Operation + +[plantuml, error-handling, svg] +.... +!theme plain + +actor "Application" as app +participant "CpuBackend" as backend +participant "ValidationUtils" as validator + +app -> backend : plus(tensorA, tensorB) +activate backend + +backend -> validator : validateShapeCompatibility(shapeA, shapeB) +activate validator + +alt shapes compatible + validator --> backend : validation passed + deactivate validator + backend -> backend : perform addition + backend --> app : result tensor +else shapes incompatible + validator --> backend : validation failed + deactivate validator + backend --> app : throw ShapeIncompatibilityException +end + +deactivate backend +.... + +*Common Error Scenarios*: +* Shape incompatibility in operations +* Invalid tensor dimensions +* Null pointer references +* Out of memory conditions + +*Error Recovery Strategies*: +* Graceful degradation with fallback operations +* Clear error messages with suggested fixes +* Resource cleanup on failure +* Performance impact logging + +== Multiplatform Deployment + +=== JVM Runtime Scenario + +[plantuml, jvm-runtime, svg] +.... +!theme plain + +package "JVM Platform" { + participant "Application" as app + participant "CpuBackend" as backend + participant "FloatArray" as array + participant "JVM Math" as math +} + +app -> backend : matrix operation +backend -> array : create native array +backend -> math : utilize JVM optimizations +math -> backend : optimized result +backend -> app : tensor result +.... + +=== Native Runtime Scenario + +[plantuml, native-runtime, svg] +.... +!theme plain + +package "Native Platform" { + participant "Application" as app + participant "CpuBackend" as backend + participant "NativeArray" as array + participant "BLAS Library" as blas +} + +app -> backend : matrix operation +backend -> array : allocate native memory +backend -> blas : call optimized routines +blas -> backend : native computation result +backend -> app : tensor result +.... + +*Platform-Specific Optimizations*: +* JVM: Leverages HotSpot optimizations and efficient garbage collection +* Native: Direct memory access and BLAS library integration +* JavaScript: WebAssembly modules for performance-critical operations + +== Lifecycle Management + +=== System Startup + +1. Backend registration and discovery +2. Platform capability detection +3. Performance baseline establishment +4. Resource pool initialization + +=== Operation Execution + +1. Input validation and preprocessing +2. Backend selection based on operation type +3. Computation execution with monitoring +4. Result validation and postprocessing + +=== Resource Cleanup + +1. Tensor memory deallocation +2. Backend resource cleanup +3. Performance metrics collection +4. Graceful shutdown procedures \ No newline at end of file diff --git a/docs/modules/arc42/pages/07-deployment-view.adoc b/docs/modules/arc42/pages/07-deployment-view.adoc new file mode 100644 index 000000000..3d3a0371b --- /dev/null +++ b/docs/modules/arc42/pages/07-deployment-view.adoc @@ -0,0 +1,254 @@ += Deployment View + +[role="arc42help"] +**** +The deployment view describes: + +1. the technical infrastructure used to execute your system, with infrastructure elements like geographical locations, environments, computers, processors, channels and net topologies as well as other infrastructure elements and + +2. the mapping of (software) building blocks to that infrastructure elements. +**** + +== Infrastructure Overview + +=== Multiplatform Deployment Architecture + +[plantuml, deployment-overview, svg] +.... +!theme plain + +node "JVM Platform" { + component "JVM Runtime" as jvm + artifact "SKaiNET-tensors-jvm.jar" as jvm_jar + component "Application Server" as app_server + + jvm --> jvm_jar + app_server --> jvm +} + +node "Native Platform" { + component "Native Runtime" as native + artifact "SKaiNET-tensors.klib" as native_lib + component "Desktop Application" as desktop + + native --> native_lib + desktop --> native +} + +node "JavaScript Platform" { + component "Node.js Runtime" as nodejs + component "Browser Engine" as browser + artifact "SKaiNET-tensors.js" as js_lib + + nodejs --> js_lib + browser --> js_lib +} + +node "Mobile Platform" { + component "Android Runtime" as android + component "iOS Runtime" as ios + artifact "SKaiNET-tensors-android.aar" as android_lib + artifact "SKaiNET-tensors.framework" as ios_lib + + android --> android_lib + ios --> ios_lib +} +.... + +== Deployment Scenarios + +=== Server-Side Deployment (JVM) + +[options="header",cols="1,2,3"] +|=== +| Component | Technology | Purpose + +| Application Server +| Spring Boot, Ktor, or standalone JVM +| Host AI/ML services and APIs + +| Tensor Operations +| SKaiNET-tensors-jvm.jar +| High-performance tensor computations + +| Hardware Acceleration +| CPU optimizations, potential GPU backends +| Maximize computational performance + +| Monitoring +| JVM metrics, GC monitoring +| Performance tracking and optimization +|=== + +*Deployment Characteristics*: +* High-performance server-grade hardware +* Multi-core CPU utilization +* Large memory allocation for tensor operations +* Potential GPU acceleration support + +=== Desktop Application Deployment (Native) + +[plantuml, desktop-deployment, svg] +.... +!theme plain + +node "Desktop Computer" { + component "Operating System" as os + component "Native Application" as app + artifact "SKaiNET-tensors.klib" as lib + component "BLAS Library" as blas + + app --> lib : "links to" + lib --> blas : "utilizes" + os --> blas : "provides" +} +.... + +*Benefits*: +* Direct hardware access and optimization +* No JVM overhead or garbage collection +* Integration with system-level math libraries +* Reduced memory footprint + +*Requirements*: +* Platform-specific compilation +* BLAS/LAPACK library availability +* Native memory management + +=== Mobile Deployment + +==== Android Deployment + +[options="header",cols="1,2"] +|=== +| Component | Description + +| Android App +| Native Android application with AI/ML features + +| SKaiNET-tensors-android.aar +| Android Archive containing compiled Kotlin code + +| Android Runtime (ART) +| Executes Kotlin bytecode with optimizations + +| Hardware Utilization +| CPU cores, potential GPU/NPU acceleration +|=== + +==== iOS Deployment + +[options="header",cols="1,2"] +|=== +| Component | Description + +| iOS App +| Native iOS application with AI/ML capabilities + +| SKaiNET-tensors.framework +| iOS Framework with Kotlin/Native compilation + +| iOS Runtime +| Direct native code execution + +| Hardware Integration +| A-series chip optimizations, Metal performance shaders +|=== + +=== Web Deployment (JavaScript) + +[plantuml, web-deployment, svg] +.... +!theme plain + +cloud "Content Delivery Network" { + artifact "SKaiNET-tensors.js" as js_lib +} + +node "Client Browser" { + component "JavaScript Engine" as js_engine + component "WebAssembly Runtime" as wasm + component "Web Application" as webapp + + webapp --> js_lib : "imports" + js_lib --> js_engine : "executes on" + js_lib --> wasm : "utilizes for performance" +} + +node "Node.js Server" { + component "Node.js Runtime" as nodejs + component "Server Application" as server_app + + server_app --> js_lib : "requires" + js_lib --> nodejs : "executes on" +} +.... + +*Deployment Options*: +* Browser-based applications with client-side ML +* Node.js server applications +* Progressive Web Apps (PWAs) +* WebAssembly for performance-critical operations + +== Infrastructure Requirements + +=== Hardware Requirements + +[options="header",cols="1,2,2,2"] +|=== +| Platform | Minimum | Recommended | Optimal + +| JVM Server +| 4 cores, 8GB RAM +| 8 cores, 16GB RAM +| 16+ cores, 32GB+ RAM, GPU + +| Desktop Native +| 2 cores, 4GB RAM +| 4 cores, 8GB RAM +| 8+ cores, 16GB+ RAM + +| Mobile +| 2GB RAM, quad-core +| 4GB RAM, octa-core +| 6GB+ RAM, flagship SoC + +| Web Browser +| Modern browser, 2GB RAM +| Chrome/Firefox, 4GB RAM +| Latest browser, 8GB+ RAM +|=== + +=== Software Dependencies + +==== JVM Platform +* Java 8+ or Kotlin/JVM runtime +* Optional: CUDA drivers for GPU acceleration +* Optional: Intel MKL for CPU optimization + +==== Native Platform +* Platform-specific C++ runtime +* BLAS/LAPACK libraries (OpenBLAS, Intel MKL) +* Platform development tools (GCC, Clang, MSVC) + +==== JavaScript Platform +* Modern JavaScript engine (V8, SpiderMonkey) +* WebAssembly support +* Node.js 14+ for server deployment + +== Scalability Considerations + +=== Horizontal Scaling +* Stateless tensor operations enable easy load balancing +* Microservice architecture with dedicated tensor services +* Container deployment with Kubernetes orchestration + +=== Vertical Scaling +* Multi-core CPU utilization through parallel algorithms +* Memory scaling for large tensor operations +* GPU acceleration for compute-intensive workloads + +=== Edge Deployment +* Mobile and embedded device support +* Reduced model sizes for edge computing +* Offline capability with local inference \ No newline at end of file diff --git a/docs/modules/arc42/pages/08-crosscutting-concepts.adoc b/docs/modules/arc42/pages/08-crosscutting-concepts.adoc new file mode 100644 index 000000000..58d3a1fa1 --- /dev/null +++ b/docs/modules/arc42/pages/08-crosscutting-concepts.adoc @@ -0,0 +1,277 @@ += Crosscutting Concepts + +[role="arc42help"] +**** +This section describes overall, principal regulations and solution ideas that are relevant in multiple parts (= cross-cutting) of your system. +Such concepts are often related to multiple building blocks. +They can include many different topics, such as + +* domain models +* architecture patterns or design patterns +* rules for using specific technology +* principal, often technical decisions of overall decisions +* implementation rules +**** + +== Type Safety and Generic Design + +=== Generic Type System + +SKaiNET employs a sophisticated generic type system to ensure compile-time type safety: + +```kotlin +interface Tensor : TensorData, TensorOps> +interface ComputeBackend : TensorOps> +``` + +*Benefits*: +* Compile-time type checking prevents runtime type errors +* Shape validation at tensor creation time +* Clear API contracts between components +* IDE support with autocompletion and refactoring + +*Implementation Pattern*: +* `T : DType` constrains data types to supported numerical types +* `V` represents the runtime value type (Float, Double, Int, Long) +* Type parameters propagate through the entire operation chain + +== Memory Management + +=== Multiplatform Memory Strategy + +[plantuml, memory-management, svg] +.... +!theme plain + +package "JVM Platform" { + component "Garbage Collector" as gc + component "FloatArray" as jvm_array + component "Tensor" as jvm_tensor + + jvm_tensor --> jvm_array : "references" + gc --> jvm_array : "manages" +} + +package "Native Platform" { + component "Manual Memory" as manual + component "NativeArray" as native_array + component "Tensor" as native_tensor + + native_tensor --> native_array : "owns" + manual --> native_array : "allocates/deallocates" +} + +package "JavaScript Platform" { + component "JS GC" as js_gc + component "TypedArray" as js_array + component "Tensor" as js_tensor + + js_tensor --> js_array : "references" + js_gc --> js_array : "manages" +} +.... + +*Platform-Specific Strategies*: +* **JVM**: Leverages garbage collection for automatic memory management +* **Native**: Manual memory management with RAII patterns +* **JavaScript**: Browser/Node.js garbage collection with typed arrays + +*Memory Optimization Techniques*: +* Array reuse for temporary computations +* Lazy initialization of large tensors +* Memory pool allocation for frequent operations +* Copy-on-write semantics where applicable + +== Performance Measurement Framework + +=== Benchmarking Architecture + +```kotlin +class BenchmarkRunner { + fun benchmark( + name: String, + warmupRuns: Int = 5, + measurementRuns: Int = 10, + operation: () -> T + ): BenchmarkResult +} +``` + +*Key Principles*: +* **Warmup Phase**: Stabilizes JVM performance and eliminates cold start effects +* **Statistical Analysis**: Computes mean, standard deviation, and percentiles +* **Outlier Detection**: Identifies and handles performance anomalies +* **Regression Testing**: Detects performance degradation in CI/CD + +*Metrics Collected*: +* Execution time with high-precision timing +* Memory allocation patterns +* Operation throughput (ops/second) +* Scaling characteristics analysis + +== Error Handling and Validation + +=== Comprehensive Error Strategy + +[plantuml, error-handling, svg] +.... +!theme plain + +package "Validation Layer" { + class "ShapeValidator" as shape_val + class "TypeValidator" as type_val + class "RangeValidator" as range_val +} + +package "Error Types" { + exception "ShapeIncompatibilityException" as shape_err + exception "InvalidDimensionException" as dim_err + exception "TypeMismatchException" as type_err +} + +package "Recovery Strategies" { + class "GracefulDegradation" as graceful + class "FallbackOperations" as fallback + class "ResourceCleanup" as cleanup +} + +shape_val --> shape_err : "throws" +type_val --> type_err : "throws" +range_val --> dim_err : "throws" + +shape_err --> graceful : "triggers" +type_err --> fallback : "triggers" +dim_err --> cleanup : "triggers" +.... + +*Error Categories*: +* **Shape Validation**: Tensor dimension compatibility checks +* **Type Safety**: Data type consistency validation +* **Resource Management**: Memory allocation and cleanup errors +* **Performance Issues**: Timeout and resource exhaustion handling + +*Error Recovery Patterns*: +* Graceful degradation with reduced functionality +* Fallback to alternative computation methods +* Clear error messages with corrective suggestions +* Automatic resource cleanup on failure + +== Logging and Observability + +=== Structured Logging Framework + +```kotlin +// Performance logging with structured format +logger.info("[PERFORMANCE] Operation: {}, Duration: {}ms, Elements: {}", + operationName, duration, elementCount) + +// Debug logging for development +logger.debug("[DEBUG_LOG] Tensor shape: {}, Backend: {}", + tensor.shape, backend.name) +``` + +*Logging Categories*: +* **Performance**: Execution times, throughput, resource usage +* **Debug**: Development and troubleshooting information +* **Error**: Exception details and stack traces +* **Audit**: Operation history and configuration changes + +*Observability Features*: +* Structured logging format for log analysis +* Performance metrics integration +* Debug mode for detailed tracing +* Production monitoring compatibility + +== Testing Strategy + +=== Multi-Level Testing Approach + +[options="header",cols="1,2,2,2"] +|=== +| Test Level | Purpose | Tools | Coverage + +| Unit Tests +| Individual component validation +| Kotlin Test, JUnit +| Functions, classes, edge cases + +| Integration Tests +| Component interaction verification +| Kotlin Test +| API contracts, backend integration + +| Performance Tests +| Benchmark execution and regression +| Custom benchmark framework +| Operation performance, scaling + +| Multiplatform Tests +| Cross-platform compatibility +| Kotlin Multiplatform Test +| Platform-specific behavior +|=== + +*Testing Principles*: +* Test-driven development with failing tests first +* Comprehensive edge case coverage +* Performance regression prevention +* Platform-specific testing where needed + +== Documentation Standards + +=== Documentation Architecture + +```kotlin +/** + * Performs matrix multiplication between two tensors. + * + * @param a First tensor (left operand) with shape [m, k] + * @param b Second tensor (right operand) with shape [k, n] + * @return Result tensor with shape [m, n] + * @throws ShapeIncompatibilityException if tensor shapes are incompatible + * + * Time Complexity: O(m * n * k) + * Space Complexity: O(m * n) + */ +fun matmul(a: Tensor, b: Tensor): Tensor +``` + +*Documentation Requirements*: +* **KDoc**: Complete API documentation for all public interfaces +* **Arc42**: Architecture documentation with this framework +* **Performance**: Complexity analysis for all operations +* **Examples**: Usage examples with common patterns + +*Quality Assurance*: +* Documentation review in pull requests +* Automated documentation generation +* Link verification and consistency checks +* Regular documentation updates with code changes + +== Multiplatform Compatibility + +=== Platform Abstraction Strategy + +```kotlin +// Common interface definition +expect class PlatformSpecificOptimizations { + fun optimizedMatmul(a: FloatArray, b: FloatArray): FloatArray +} + +// Platform-specific implementations +// JVM: Uses optimized libraries +// Native: Uses BLAS/LAPACK +// JS: Uses WebAssembly modules +``` + +*Compatibility Principles*: +* Common interface with platform-specific implementations +* Expect/actual declarations for platform differences +* Shared business logic with optimized platform code +* Consistent API across all target platforms + +*Platform-Specific Optimizations*: +* JVM: HotSpot compiler optimizations, efficient GC +* Native: Direct memory access, BLAS integration +* JavaScript: WebAssembly for performance-critical code +* Mobile: Platform-specific acceleration (Metal, Vulkan) \ No newline at end of file diff --git a/docs/modules/arc42/pages/09-architecture-decisions.adoc b/docs/modules/arc42/pages/09-architecture-decisions.adoc new file mode 100644 index 000000000..b18b3f038 --- /dev/null +++ b/docs/modules/arc42/pages/09-architecture-decisions.adoc @@ -0,0 +1,318 @@ += Architecture Decisions + +[role="arc42help"] +**** +Important, expensive, large scale or risky architecture decisions including rationals. +With "decisions" we mean selecting one alternative based on given criteria. + +Please use your judgement to decide whether an architectural decision should be documented here in this central section or whether you better document it locally (e.g. within the white box template of one building block). + +Avoid redundancy. Refer to section 4, where you already captured the most important decisions of your architecture. +**** + +== Decision Records + +=== ADR-001: Kotlin Multiplatform as Primary Technology + +*Status*: Accepted + +*Date*: 2024 + +*Context*: +Need to choose a technology stack that enables cross-platform deployment while maintaining type safety and performance for tensor operations. + +*Decision*: +Use Kotlin Multiplatform as the primary development technology for the entire framework. + +*Consequences*: + +*Positive*: +* Single codebase with platform-specific optimizations +* Strong type safety with compile-time error detection +* Growing ecosystem with excellent tooling support +* Seamless integration with existing JVM and Android projects +* Modern language features improving developer productivity + +*Negative*: +* Limited ecosystem compared to established ML frameworks +* Platform-specific debugging can be challenging +* Compilation times may be longer for multiplatform builds +* Requires team expertise in Kotlin and multiplatform concepts + +=== ADR-002: Pluggable Backend Architecture + +*Status*: Accepted + +*Date*: 2024 + +*Context*: +Different hardware platforms (CPU, GPU, TPU) require specialized optimizations, but the API should remain consistent across platforms. + +*Decision*: +Implement a pluggable backend architecture with the `ComputeBackend` interface as the abstraction layer. + +*Rationale*: +* Enables hardware-specific optimizations without API changes +* Facilitates performance comparison between implementations +* Supports future hardware platforms without framework modifications +* Allows runtime backend selection based on availability + +*Consequences*: + +*Positive*: +* Clean separation between API and implementation +* Easy integration of specialized libraries (BLAS, CUDA, etc.) +* Performance optimization opportunities for each platform +* Future-proof architecture for emerging hardware + +*Negative*: +* Additional abstraction layer may introduce overhead +* More complex testing matrix across different backends +* Potential for inconsistent behavior between backends + +=== ADR-003: Comprehensive Performance Measurement Framework + +*Status*: Accepted + +*Date*: 2024 + +*Context*: +Performance is critical for machine learning workloads, requiring systematic measurement and optimization. + +*Decision*: +Integrate comprehensive performance benchmarking directly into the framework with `BenchmarkRunner` and automated regression testing. + +*Rationale*: +* Performance regression detection in CI/CD pipeline +* Systematic comparison of optimization attempts +* Data-driven performance optimization decisions +* Baseline establishment for different hardware platforms + +*Implementation Details*: +```kotlin +class BenchmarkRunner { + fun benchmark( + name: String, + warmupRuns: Int, + measurementRuns: Int, + operation: () -> T + ): BenchmarkResult +} +``` + +*Consequences*: + +*Positive*: +* Early detection of performance regressions +* Systematic performance optimization approach +* Clear performance expectations for users +* Data-driven architecture decisions + +*Negative*: +* Increased build time due to benchmark execution +* Additional complexity in testing infrastructure +* Resource requirements for comprehensive benchmarking + +=== ADR-004: Generic Type System with Compile-Time Validation + +*Status*: Accepted + +*Date*: 2024 + +*Context*: +Tensor operations are prone to shape and type mismatches that traditionally cause runtime errors. + +*Decision*: +Leverage Kotlin's generic type system for compile-time validation of tensor operations. + +*Design*: +```kotlin +interface Tensor : TensorData, TensorOps> +interface ComputeBackend : TensorOps> +``` + +*Rationale*: +* Prevents common tensor operation errors at compile time +* Improves developer experience with IDE support +* Clear API contracts and documentation +* Type-safe operations without runtime overhead + +*Consequences*: + +*Positive*: +* Eliminates entire classes of runtime errors +* Better IDE support (autocompletion, refactoring) +* Self-documenting APIs through type signatures +* Performance optimization opportunities + +*Negative*: +* Steeper learning curve for developers unfamiliar with generics +* More complex API signatures +* Potential compilation time increase + +=== ADR-005: Module Separation: API vs Implementation + +*Status*: Accepted + +*Date*: 2024 + +*Context*: +Need clear boundaries between public contracts and implementation details to enable independent evolution. + +*Decision*: +Separate the framework into distinct modules: +* `SKaiNET-tensors-api`: Pure interfaces and contracts +* `SKaiNET-tensors`: Reference CPU implementation +* Future: `SKaiNET-gpu`, `SKaiNET-distributed`, etc. + +*Rationale*: +* Clear API boundaries and contracts +* Independent evolution of implementations +* Easier testing and mocking capabilities +* Reduced compilation dependencies + +*Consequences*: + +*Positive*: +* Clean architecture with well-defined boundaries +* Multiple implementations possible without API changes +* Better testability with interface mocking +* Faster incremental compilation + +*Negative*: +* Additional complexity in project structure +* More modules to maintain and version +* Potential for API/implementation version mismatches + +=== ADR-006: Memory Management Strategy + +*Status*: Accepted + +*Date*: 2024 + +*Context*: +Different platforms have different memory management characteristics requiring platform-specific approaches. + +*Decision*: +Adopt platform-specific memory management strategies: +* JVM: Leverage garbage collection with array reuse optimization +* Native: Manual memory management with RAII patterns +* JavaScript: Browser GC with TypedArray optimizations + +*Rationale*: +* Optimal performance on each platform +* Leverages platform-specific optimizations +* Consistent API despite different underlying strategies + +*Consequences*: + +*Positive*: +* Platform-optimized performance characteristics +* Reduced memory fragmentation on native platforms +* Efficient garbage collection on managed platforms + +*Negative*: +* Platform-specific debugging requirements +* Different performance characteristics across platforms +* Increased complexity in memory profiling + +=== ADR-007: Operator Overloading for Mathematical Operations + +*Status*: Accepted + +*Date*: 2024 + +*Context*: +Mathematical tensor operations should feel natural and intuitive to developers familiar with mathematical notation. + +*Decision*: +Implement operator overloading for common mathematical operations: + +```kotlin +val c = a + b // Element-wise addition +val d = a * b // Element-wise multiplication +val e = a.matmul(b) // Matrix multiplication +val f = a + 5.0 // Scalar addition +``` + +*Rationale*: +* Intuitive mathematical syntax +* Reduced boilerplate code +* Familiar to developers from other ML frameworks +* Clear distinction between operations (+ vs matmul) + +*Consequences*: + +*Positive*: +* Improved developer experience and code readability +* Reduced learning curve for users familiar with NumPy/PyTorch +* Less verbose mathematical expressions +* Natural mathematical notation + +*Negative*: +* Potential confusion between element-wise and matrix operations +* Operator precedence considerations +* May hide computational complexity + +== Rejected Alternatives + +=== Alternative: Pure Java Implementation + +*Rejected*: Using pure Java for broader compatibility + +*Reasons*: +* Missing modern language features (null safety, coroutines) +* Verbose syntax reducing developer productivity +* No multiplatform capabilities +* Limited type system compared to Kotlin + +=== Alternative: Single Monolithic Module + +*Rejected*: Combining API and implementation in single module + +*Reasons*: +* Poor separation of concerns +* Difficult to provide alternative implementations +* Tight coupling between interface and implementation +* Harder to test and mock components + +=== Alternative: Dynamic Type System + +*Rejected*: Runtime type checking instead of compile-time generics + +*Reasons*: +* Runtime errors instead of compile-time safety +* Poor IDE support and tooling +* Performance overhead of runtime type checks +* Less clear API contracts + +== Future Decisions + +=== Pending: GPU Backend Implementation Strategy + +*Status*: Under consideration + +*Options*: +* CUDA integration for NVIDIA GPUs +* OpenCL for cross-platform GPU support +* Platform-specific solutions (Metal, Vulkan) + +*Criteria*: +* Performance characteristics +* Platform compatibility +* Development complexity +* Ecosystem integration + +=== Pending: Distributed Computing Support + +*Status*: Under consideration + +*Options*: +* Built-in distributed tensor operations +* Integration with existing frameworks (Apache Spark) +* Custom clustering solution + +*Considerations*: +* Network communication overhead +* Data partitioning strategies +* Fault tolerance requirements \ No newline at end of file diff --git a/docs/modules/arc42/pages/10-quality-requirements.adoc b/docs/modules/arc42/pages/10-quality-requirements.adoc new file mode 100644 index 000000000..a6550b772 --- /dev/null +++ b/docs/modules/arc42/pages/10-quality-requirements.adoc @@ -0,0 +1,370 @@ += Quality Requirements + +[role="arc42help"] +**** +This section contains all quality requirements as quality tree with scenarios. The most important ones have already been described in section 1.2. (quality goals) + +Here you can also capture quality requirements with lesser priority, which will not create high architectural impact. Consider this section as a structured collection of requirements. Use it also to capture architecturally relevant quality requirements that you might not have thought of when writing section 1.2. +**** + +== Quality Tree + +[plantuml, quality-tree, svg] +.... +!theme plain + +map "SKaiNET Quality Requirements" as quality { + Performance => High Priority + Type Safety => High Priority + Extensibility => High Priority + Developer Experience => High Priority + Cross-platform Compatibility => High Priority + Maintainability => Medium Priority + Security => Medium Priority + Documentation => Medium Priority + Testability => Medium Priority +} + +map "Performance" as perf { + Execution Speed => Matrix operations competitive with NumPy + Memory Efficiency => Optimal memory usage across platforms + Scalability => Linear scaling for element-wise operations + Throughput => High operations per second +} + +map "Type Safety" as safety { + Compile-time Validation => Shape and type checking at compile time + Runtime Stability => Zero tensor-related runtime crashes + Error Prevention => Clear compiler errors for invalid operations +} + +map "Developer Experience" as dx { + API Intuitiveness => Mathematical notation similarity + IDE Support => Full autocompletion and refactoring + Learning Curve => Easy adoption for ML developers + Documentation Quality => Complete and accurate documentation +} + +quality --> perf +quality --> safety +quality --> dx +.... + +== Quality Scenarios + +=== Performance Requirements + +==== Scenario P-1: Matrix Multiplication Performance + +[options="header",cols="1,3"] +|=== +| Attribute | Value + +| Scenario +| Large matrix multiplication (1000x1000) on modern CPU + +| Stimulus +| Application requests matrix multiplication of two 1000x1000 FP32 matrices + +| Environment +| Production server with 8-core CPU, 16GB RAM + +| Response +| Matrix multiplication completes successfully + +| Response Measure +| Execution time ≤ 2 seconds, competitive with NumPy baseline +|=== + +==== Scenario P-2: Memory Efficiency + +[options="header",cols="1,3"] +|=== +| Attribute | Value + +| Scenario +| Tensor operations with constrained memory + +| Stimulus +| Series of tensor operations creating intermediate results + +| Environment +| Mobile device with 4GB RAM, 2GB available to application + +| Response +| Operations complete without out-of-memory errors + +| Response Measure +| Peak memory usage ≤ 1.5x theoretical minimum, efficient garbage collection +|=== + +==== Scenario P-3: Scalability + +[options="header",cols="1,3"] +|=== +| Attribute | Value + +| Scenario +| Element-wise operations scaling + +| Stimulus +| Element-wise addition with tensors of varying sizes (1K to 10M elements) + +| Environment +| Standard development machine + +| Response +| Operations complete with predictable scaling + +| Response Measure +| Execution time scales linearly O(n) with element count ±20% +|=== + +=== Type Safety Requirements + +==== Scenario TS-1: Compile-Time Shape Validation + +[options="header",cols="1,3"] +|=== +| Attribute | Value + +| Scenario +| Invalid matrix multiplication at compile time + +| Stimulus +| Developer attempts to multiply incompatible matrices (e.g., [3,4] × [2,3]) + +| Environment +| Development environment with IDE + +| Response +| Compilation fails with clear error message + +| Response Measure +| 100% of shape incompatibilities caught at compile time, clear error descriptions +|=== + +==== Scenario TS-2: Type System Integration + +[options="header",cols="1,3"] +|=== +| Attribute | Value + +| Scenario +| IDE support for tensor operations + +| Stimulus +| Developer types tensor operations in IDE + +| Environment +| IntelliJ IDEA or VS Code with Kotlin plugin + +| Response +| Full IDE support with type information + +| Response Measure +| Autocompletion accuracy >95%, immediate error highlighting, refactoring support +|=== + +=== Developer Experience Requirements + +==== Scenario DX-1: API Learning Curve + +[options="header",cols="1,3"] +|=== +| Attribute | Value + +| Scenario +| New developer adopting SKaiNET + +| Stimulus +| ML engineer familiar with NumPy/PyTorch starts using SKaiNET + +| Environment +| Standard development setup with documentation + +| Response +| Successfully implements basic tensor operations + +| Response Measure +| Productive within 2 hours, implements matrix operations within 30 minutes +|=== + +==== Scenario DX-2: Mathematical Notation Similarity + +[options="header",cols="1,3"] +|=== +| Attribute | Value + +| Scenario +| Complex mathematical expression implementation + +| Stimulus +| Developer implements: `result = (A * B + C) / scalar` + +| Environment +| Development with operator overloading + +| Response +| Natural mathematical syntax works as expected + +| Response Measure +| Syntax matches mathematical notation >90%, no unexpected behavior +|=== + +=== Cross-Platform Compatibility Requirements + +==== Scenario CP-1: Multiplatform Consistency + +[options="header",cols="1,3"] +|=== +| Attribute | Value + +| Scenario +| Same code running on different platforms + +| Stimulus +| Identical tensor operations executed on JVM, Native, and JavaScript + +| Environment +| JVM server, native desktop app, Node.js application + +| Response +| Consistent results across all platforms + +| Response Measure +| Numerical results identical within floating-point precision, API behavior consistent +|=== + +==== Scenario CP-2: Platform-Specific Optimization + +[options="header",cols="1,3"] +|=== +| Attribute | Value + +| Scenario +| Hardware optimization utilization + +| Stimulus +| Matrix multiplication on platform with optimized BLAS + +| Environment +| Native platform with Intel MKL or similar + +| Response +| Automatic utilization of optimized libraries + +| Response Measure +| Performance improvement ≥50% over generic implementation where available +|=== + +== Quality Attributes Detail + +=== Performance + +*Objective*: Enable high-performance tensor computations competitive with established frameworks. + +*Measurements*: +* Execution time benchmarks against NumPy, PyTorch baselines +* Memory usage profiling and optimization +* Throughput measurements (operations per second) +* Scaling analysis for different tensor sizes + +*Architecture Impact*: +* Backend abstraction enables hardware-specific optimizations +* Performance measurement framework integrated into development process +* Memory management strategies optimized per platform + +=== Type Safety + +*Objective*: Eliminate tensor-related runtime errors through compile-time validation. + +*Measurements*: +* Percentage of errors caught at compile time vs runtime +* IDE integration quality metrics +* Developer error reporting satisfaction + +*Architecture Impact*: +* Generic type system with constrained type parameters +* Shape validation at tensor creation +* Comprehensive error messages and suggestions + +=== Extensibility + +*Objective*: Support easy integration of new backends and operations. + +*Measurements*: +* Time to implement new backend +* Lines of code required for new operations +* Backward compatibility maintenance + +*Architecture Impact*: +* Pluggable backend architecture +* Clean separation between API and implementation +* Service provider interface (SPI) patterns + +=== Developer Experience + +*Objective*: Provide intuitive, well-documented APIs that enhance productivity. + +*Measurements*: +* Time to first successful tensor operation +* API discoverability through IDE features +* Documentation completeness and accuracy + +*Architecture Impact*: +* Operator overloading for mathematical notation +* Comprehensive KDoc documentation +* Examples and tutorials integrated with API + +== Performance Benchmarks and Targets + +=== Baseline Performance Targets + +[options="header",cols="1,2,2,2"] +|=== +| Operation | Small (100x100) | Medium (500x500) | Large (1000x1000) + +| Matrix Multiplication +| ≤ 10ms +| ≤ 200ms +| ≤ 2000ms + +| Element-wise Addition +| ≤ 1ms +| ≤ 10ms +| ≤ 50ms + +| Tensor Creation +| ≤ 1ms +| ≤ 5ms +| ≤ 20ms + +| Memory Usage +| ≤ 2x theoretical +| ≤ 1.8x theoretical +| ≤ 1.5x theoretical +|=== + +=== Platform-Specific Performance Expectations + +* **JVM**: Competitive with optimized Java math libraries +* **Native**: Superior performance through BLAS integration +* **JavaScript**: Reasonable performance with WebAssembly acceleration +* **Mobile**: Efficient execution within mobile constraints + +== Quality Assurance Measures + +=== Automated Quality Gates + +1. **Performance Regression Testing**: Automated benchmarks in CI/CD +2. **Type Safety Validation**: Comprehensive compile-time test coverage +3. **Cross-Platform Testing**: Identical behavior validation across platforms +4. **API Usability Testing**: Documentation examples as executable tests + +=== Continuous Monitoring + +1. **Performance Metrics**: Real-time performance data collection +2. **Error Rate Tracking**: Runtime error frequency monitoring +3. **API Usage Analysis**: Common usage patterns and pain points +4. **Developer Feedback**: Regular surveys and feedback collection \ No newline at end of file diff --git a/docs/modules/arc42/pages/11-risks-and-technical-debt.adoc b/docs/modules/arc42/pages/11-risks-and-technical-debt.adoc new file mode 100644 index 000000000..1abeb7963 --- /dev/null +++ b/docs/modules/arc42/pages/11-risks-and-technical-debt.adoc @@ -0,0 +1,337 @@ += Risks and Technical Debt + +[role="arc42help"] +**** +A list of identified technical risks or technical debts, ordered by priority +**** + +== High Priority Risks + +=== R-001: Performance Degradation Risk + +*Risk Description*: Framework performance significantly lags behind established alternatives (NumPy, PyTorch). + +*Probability*: Medium +*Impact*: High +*Risk Level*: High + +*Root Causes*: +* Lack of hardware-specific optimizations +* Inefficient memory management patterns +* Suboptimal algorithm implementations +* Abstraction layer overhead + +*Potential Consequences*: +* Poor adoption by performance-critical applications +* Negative reputation in ML community +* Development effort abandonment +* Competitive disadvantage + +*Mitigation Strategies*: +1. **Comprehensive Benchmarking**: Continuous performance comparison against baselines +2. **Hardware-Specific Backends**: Develop GPU, TPU, and optimized CPU backends +3. **Algorithm Optimization**: Implement state-of-the-art numerical algorithms +4. **Memory Pool Management**: Reduce allocation overhead with object pools +5. **SIMD Optimizations**: Leverage vectorized instructions where available + +*Status*: Active mitigation through performance measurement framework + +=== R-002: Kotlin Multiplatform Ecosystem Maturity + +*Risk Description*: Kotlin Multiplatform ecosystem limitations impact development and deployment. + +*Probability*: Medium +*Impact*: High +*Risk Level*: High + +*Specific Concerns*: +* Limited third-party library support +* Platform-specific debugging challenges +* Compilation toolchain stability +* Performance characteristics variance across platforms + +*Potential Consequences*: +* Development velocity reduction +* Platform-specific bugs and inconsistencies +* Increased maintenance overhead +* Limited integration options + +*Mitigation Strategies*: +1. **Gradual Adoption**: Start with JVM platform and expand gradually +2. **Platform-Specific Testing**: Comprehensive test coverage for each platform +3. **Community Engagement**: Active participation in Kotlin Multiplatform community +4. **Fallback Plans**: Maintain ability to fall back to platform-specific implementations +5. **Toolchain Updates**: Regular updates to latest stable versions + +*Status*: Monitored with contingency plans + +=== R-003: API Design Complexity + +*Risk Description*: Generic type system creates overly complex APIs that hinder adoption. + +*Probability*: Medium +*Impact*: Medium +*Risk Level*: Medium-High + +*Complexity Sources*: +* Generic type parameters `Tensor` +* Backend abstraction requirements +* Type-safe shape validation +* Operator overloading interactions + +*Potential Consequences*: +* Steep learning curve for developers +* Reduced IDE support effectiveness +* Complex error messages +* Documentation challenges + +*Mitigation Strategies*: +1. **API Simplification**: Provide simplified APIs for common use cases +2. **Type Aliases**: Create convenient type aliases for frequent patterns +3. **Documentation Excellence**: Comprehensive examples and tutorials +4. **IDE Plugin Development**: Custom IDE support for better developer experience +5. **User Feedback**: Regular API usability testing with target users + +*Status*: Continuous monitoring through developer feedback + +== Medium Priority Risks + +=== R-004: Memory Management Inconsistencies + +*Risk Description*: Different memory management strategies across platforms cause unexpected behavior. + +*Probability*: Medium +*Impact*: Medium +*Risk Level*: Medium + +*Platform Differences*: +* JVM garbage collection vs Native manual management +* JavaScript TypedArray limitations +* Mobile platform memory constraints + +*Mitigation Strategies*: +* Comprehensive cross-platform testing +* Clear documentation of platform differences +* Memory usage guidelines per platform +* Automated memory leak detection + +=== R-005: Third-Party Integration Challenges + +*Risk Description*: Difficulty integrating with existing ML/AI ecosystem tools and frameworks. + +*Probability*: Low-Medium +*Impact*: Medium +*Risk Level*: Medium + +*Integration Points*: +* Data format compatibility (HDF5, ONNX, etc.) +* Interoperability with existing frameworks +* Build system integration +* Deployment pipeline compatibility + +*Mitigation Strategies*: +* Standard format support development +* Bridge libraries for popular frameworks +* Comprehensive integration documentation +* Community contribution encouragement + +=== R-006: Security Vulnerabilities + +*Risk Description*: Security issues in tensor operations or dependencies. + +*Probability*: Low +*Impact*: Medium-High +*Risk Level*: Medium + +*Security Concerns*: +* Memory buffer overflows in native code +* Dependency vulnerabilities +* Unsafe deserialization of tensor data +* Resource exhaustion attacks + +*Mitigation Strategies*: +* Security-focused code reviews +* Automated vulnerability scanning +* Safe deserialization practices +* Resource usage limits and monitoring + +== Low Priority Risks + +=== R-007: Community Adoption Challenges + +*Risk Description*: Low community adoption leads to project stagnation. + +*Probability*: Low-Medium +*Impact*: Medium +*Risk Level*: Low-Medium + +*Adoption Barriers*: +* Established framework dominance (PyTorch, TensorFlow) +* Limited ecosystem of compatible tools +* Documentation and tutorial gaps +* Performance perception issues + +*Mitigation Strategies*: +* Strong marketing and community outreach +* Showcase projects and success stories +* Conference presentations and publications +* Open source contribution facilitation + +=== R-008: Maintenance Overhead + +*Risk Description*: High maintenance costs due to multiplatform complexity. + +*Probability*: Medium +*Impact*: Low-Medium +*Risk Level*: Low-Medium + +*Maintenance Challenges*: +* Platform-specific bug fixes +* Dependency updates across platforms +* Performance regression testing +* Documentation synchronization + +*Mitigation Strategies*: +* Automated testing and CI/CD pipelines +* Clear contribution guidelines +* Modular architecture for isolated changes +* Community contributor onboarding + +== Technical Debt + +=== TD-001: Limited Backend Implementations + +*Description*: Currently only CPU backend is implemented, limiting performance potential. + +*Technical Impact*: +* Performance bottleneck for large computations +* Limited hardware utilization +* Competitive disadvantage + +*Business Impact*: +* Reduced adoption for performance-critical applications +* Missing market opportunities in GPU-accelerated computing + +*Resolution Plan*: +1. GPU backend development (CUDA/OpenCL) +2. TPU backend investigation +3. Mobile-optimized backends +4. Distributed computing backend + +*Estimated Effort*: 6-12 months per backend +*Priority*: High + +=== TD-002: Incomplete Error Handling + +*Description*: Error handling and recovery mechanisms are not fully comprehensive. + +*Technical Issues*: +* Inconsistent error types across operations +* Limited error recovery options +* Unclear error messages in some cases +* Missing validation in edge cases + +*Resolution Plan*: +1. Standardize error hierarchy +2. Implement comprehensive validation +3. Improve error message clarity +4. Add error recovery mechanisms + +*Estimated Effort*: 2-3 months +*Priority*: Medium + +=== TD-003: Performance Optimization Gaps + +*Description*: Several optimization opportunities remain unimplemented. + +*Optimization Areas*: +* Memory pooling for frequent allocations +* SIMD instruction utilization +* Cache-aware algorithm implementations +* Parallel processing opportunities + +*Resolution Plan*: +1. Implement memory pool management +2. Add SIMD optimizations where applicable +3. Optimize data layout for cache efficiency +4. Introduce parallelization for suitable operations + +*Estimated Effort*: 4-6 months +*Priority*: High + +=== TD-004: Documentation Completeness + +*Description*: Some areas of documentation need improvement and expansion. + +*Documentation Gaps*: +* Advanced usage examples +* Performance tuning guides +* Migration guides from other frameworks +* Architecture decision rationale + +*Resolution Plan*: +1. Comprehensive API documentation review +2. Tutorial and example expansion +3. Performance guide development +4. Migration guide creation + +*Estimated Effort*: 2-3 months +*Priority*: Medium + +== Risk Monitoring and Review + +=== Risk Assessment Process + +1. **Monthly Risk Review**: Regular assessment of risk probability and impact +2. **Performance Monitoring**: Continuous tracking of performance benchmarks +3. **Community Feedback**: Regular collection and analysis of user feedback +4. **Competitive Analysis**: Monitoring of competing frameworks and technologies + +=== Risk Mitigation Tracking + +[options="header",cols="1,2,2,1,1"] +|=== +| Risk ID | Current Status | Next Review Date | Owner | Priority + +| R-001 +| Active mitigation through benchmarking +| Monthly +| Performance Team +| High + +| R-002 +| Monitoring with contingency plans +| Quarterly +| Architecture Team +| High + +| R-003 +| Continuous user feedback collection +| Bi-weekly +| API Design Team +| Medium-High + +| R-004 +| Cross-platform testing expansion +| Monthly +| QA Team +| Medium + +| R-005 +| Integration planning phase +| Quarterly +| Ecosystem Team +| Medium +|=== + +=== Escalation Criteria + +* **High Priority**: Immediate action required, weekly status updates +* **Medium Priority**: Action within 30 days, monthly reviews +* **Low Priority**: Action within quarter, quarterly reviews + +Risk escalation triggers: +* Performance degradation >20% from baseline +* Platform compatibility issues affecting >50% of users +* Security vulnerabilities with CVSS score >7.0 +* Community adoption decline for >3 consecutive months \ No newline at end of file diff --git a/docs/modules/arc42/pages/12-glossary.adoc b/docs/modules/arc42/pages/12-glossary.adoc new file mode 100644 index 000000000..8ac8e7966 --- /dev/null +++ b/docs/modules/arc42/pages/12-glossary.adoc @@ -0,0 +1,166 @@ += Glossary + +[role="arc42help"] +**** +The most important domain and technical terms used in this architecture document. +**** + +== Domain Terms + +**AI/ML (Artificial Intelligence/Machine Learning)**:: +Computing approaches that enable systems to learn patterns from data and make predictions or decisions. SKaiNET provides the computational foundation for building AI/ML applications. + +**Deep Learning Framework**:: +Software libraries and tools that provide building blocks for creating, training, and deploying neural networks. Examples include TensorFlow, PyTorch, and SKaiNET. + +**Neural Network**:: +Computing systems inspired by biological neural networks, composed of interconnected nodes (neurons) that process and transform data through weighted connections. + +**Tensor**:: +A multi-dimensional array that generalizes scalars (0D), vectors (1D), and matrices (2D) to higher dimensions. The fundamental data structure in machine learning computations. + +**Broadcasting**:: +A technique for performing operations on tensors with different but compatible shapes by automatically expanding smaller tensors to match larger ones. + +**Element-wise Operation**:: +Mathematical operations applied to corresponding elements of tensors, such as addition, multiplication, or division between two tensors of the same shape. + +**Matrix Multiplication (MatMul)**:: +A mathematical operation that combines two matrices to produce a result matrix, where each element is computed as the dot product of corresponding row and column vectors. + +== Technical Terms + +**Backend**:: +The computational engine responsible for executing tensor operations on specific hardware platforms (CPU, GPU, TPU). SKaiNET uses pluggable backends for hardware abstraction. + +**BLAS (Basic Linear Algebra Subprograms)**:: +Standardized low-level routines for common linear algebra operations. Platform-specific implementations (Intel MKL, OpenBLAS) provide optimized performance. + +**Compute Backend**:: +In SKaiNET, the `ComputeBackend` interface that abstracts tensor operations across different hardware platforms while maintaining consistent API behavior. + +**CPU Backend**:: +SKaiNET's reference implementation (`CpuBackend`) that executes tensor operations on standard processors using optimized array operations and mathematical libraries. + +**Data Type (DType)**:: +Sealed interface in SKaiNET defining supported numerical data types (FP32, FP64, INT32, INT64) with compile-time type safety and platform-specific optimizations. + +**Generic Type System**:: +Kotlin's parameterized types used in SKaiNET for compile-time type safety. Example: `Tensor` where `T` constrains data types and `V` represents runtime values. + +**JIT (Just-In-Time) Compilation**:: +Runtime compilation technique that optimizes code during execution. JVM's HotSpot compiler applies JIT optimizations to frequently executed tensor operations. + +**Kotlin Multiplatform**:: +Technology enabling shared Kotlin code across multiple platforms (JVM, Native, JavaScript) while allowing platform-specific optimizations and implementations. + +**Memory Pool**:: +Pre-allocated memory regions used to reduce allocation overhead in high-frequency operations. Particularly important for tensor operations creating many temporary objects. + +**Operator Overloading**:: +Language feature allowing custom definitions for standard operators (+, -, *, /) on user-defined types. SKaiNET uses this for intuitive mathematical notation. + +**Shape**:: +Immutable representation of tensor dimensions as a list of integers. Examples: `[3, 4]` for a 3×4 matrix, `[100, 28, 28]` for a batch of 28×28 images. + +**SIMD (Single Instruction, Multiple Data)**:: +Processor instructions that perform the same operation on multiple data elements simultaneously, enabling vectorized computations for improved performance. + +== Architecture Terms + +**API (Application Programming Interface)**:: +Public interfaces and contracts that define how applications interact with SKaiNET. Separated into `SKaiNET-tensors-api` module for clean abstraction. + +**Arc42**:: +Template for architecture documentation providing standardized sections for describing software architectures. This document follows Arc42 structure. + +**Benchmarking Framework**:: +SKaiNET's integrated performance measurement system (`BenchmarkRunner`, `TensorBenchmarkSuite`) for systematic performance analysis and regression detection. + +**Building Block**:: +Architectural components that can be combined to create larger systems. In SKaiNET: tensors, backends, operations, and performance measurement components. + +**Cross-cutting Concerns**:: +Architectural aspects that affect multiple components, such as error handling, logging, performance measurement, and memory management. + +**GitFlow**:: +Branching model used in SKaiNET development with specific branch types: master, develop, feature, release, and hotfix branches for structured release management. + +**Pluggable Architecture**:: +Design pattern allowing runtime selection and substitution of components. SKaiNET's backend system exemplifies this with hardware-specific implementations. + +**Quality Attributes**:: +System characteristics like performance, reliability, maintainability, and usability that drive architectural decisions and constraints. + +**Service Provider Interface (SPI)**:: +Design pattern for enabling service discovery and pluggability. SKaiNET uses SPI concepts for backend registration and selection. + +== Platform Terms + +**Android Runtime (ART)**:: +Android's application runtime that executes Kotlin bytecode. SKaiNET targets ART through Kotlin/JVM compilation for Android applications. + +**JavaScript Engine**:: +Runtime environment for executing JavaScript code (V8, SpiderMonkey). SKaiNET compiles to JavaScript for browser and Node.js deployment. + +**JVM (Java Virtual Machine)**:: +Runtime environment executing bytecode from Kotlin/Java compilation. Provides garbage collection, JIT optimization, and extensive ecosystem integration. + +**Kotlin/Native**:: +Kotlin compilation target producing native binaries without virtual machine overhead. Enables direct hardware access and integration with system libraries. + +**Native Binary**:: +Compiled executable code specific to target processor architecture, providing optimal performance through direct hardware utilization. + +**WebAssembly (WASM)**:: +Binary instruction format enabling near-native performance in web browsers. SKaiNET can leverage WASM for performance-critical JavaScript operations. + +== Performance Terms + +**Asymptotic Complexity**:: +Mathematical description of algorithm performance scaling. Matrix multiplication has O(n³) complexity, element-wise operations are O(n). + +**Baseline Performance**:: +Reference performance measurements against which optimizations are compared. SKaiNET establishes baselines through comprehensive benchmarking. + +**Cache Efficiency**:: +Algorithm design considering processor cache hierarchy for optimal data access patterns, crucial for large tensor operations. + +**Memory Footprint**:: +Amount of memory consumed by applications during execution. Important consideration for mobile and resource-constrained deployments. + +**Performance Regression**:: +Degradation in system performance compared to previous versions. SKaiNET's CI/CD pipeline includes automated regression detection. + +**Throughput**:: +Number of operations completed per unit time, measured in operations per second (ops/sec) or similar metrics. + +== Development Terms + +**Continuous Integration/Continuous Deployment (CI/CD)**:: +Development practices involving automated building, testing, and deployment. SKaiNET uses CI/CD for quality assurance and release management. + +**KDoc**:: +Kotlin's documentation format similar to Javadoc, used for API documentation with structured comments and annotations. + +**Semantic Versioning (SemVer)**:: +Versioning scheme (MAJOR.MINOR.PATCH) providing clear expectations about API changes and backward compatibility. + +**Test-Driven Development (TDD)**:: +Development methodology writing tests before implementation code. Ensures comprehensive test coverage and validates requirements. + +== Abbreviations and Acronyms + +**ADR**:: Architecture Decision Record +**API**:: Application Programming Interface +**CPU**:: Central Processing Unit +**GPU**:: Graphics Processing Unit +**IDE**:: Integrated Development Environment +**JIT**:: Just-In-Time (compilation) +**JVM**:: Java Virtual Machine +**ML**:: Machine Learning +**NPU**:: Neural Processing Unit +**ONNX**:: Open Neural Network Exchange +**SPI**:: Service Provider Interface +**TPU**:: Tensor Processing Unit +**WASM**:: WebAssembly \ No newline at end of file From 1921b7499c84109d626caeefb86106d1ef47d6f2 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 10:51:53 +0200 Subject: [PATCH 06/16] Refactor packagename to folow: sk.ainet.xx convetion Related-To: #93 --- .../core/tensor/ComputeBackend.kt | 0 .../sk/{ai/net => ainet}/core/tensor/DType.kt | 0 .../sk/{ai/net => ainet}/core/tensor/Shape.kt | 0 .../{ai/net => ainet}/core/tensor/Tensor.kt | 1 + .../net => ainet}/core/tensor/TensorData.kt | 0 .../net => ainet}/core/tensor/TensorOps.kt | 0 .../skainet-tensors/gradle.properties | 2 +- .../core/tensor/backend/CpuBackend.kt | 157 +++++++++++++----- .../sk/ainet/core/tensor/PrintingTest.kt | 125 ++++++++++++++ .../net => ainet}/core/tensor/TestUtils.kt | 0 .../core/tensor/backend/CpuBackendTest.kt | 0 .../benchmark/BaselinePerformanceTest.kt | 0 .../core/tensor/benchmark/BenchmarkRunner.kt | 0 .../tensor/benchmark/TensorBenchmarkSuite.kt | 0 .../tensor/multiplication/EdgeCasesTest.kt | 0 .../ElementwiseMultiplicationTest.kt | 0 .../tensor/multiplication/MLOperationsTest.kt | 0 .../MatrixMultiplicationTest.kt | 8 +- 18 files changed, 242 insertions(+), 51 deletions(-) rename skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/{ai/net => ainet}/core/tensor/ComputeBackend.kt (100%) rename skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/{ai/net => ainet}/core/tensor/DType.kt (100%) rename skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/{ai/net => ainet}/core/tensor/Shape.kt (100%) rename skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/{ai/net => ainet}/core/tensor/Tensor.kt (99%) rename skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/{ai/net => ainet}/core/tensor/TensorData.kt (100%) rename skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/{ai/net => ainet}/core/tensor/TensorOps.kt (100%) rename skainet-core/skainet-tensors/src/commonMain/kotlin/sk/{ai/net => ainet}/core/tensor/backend/CpuBackend.kt (85%) create mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt rename skainet-core/skainet-tensors/src/commonTest/kotlin/sk/{ai/net => ainet}/core/tensor/TestUtils.kt (100%) rename skainet-core/skainet-tensors/src/commonTest/kotlin/sk/{ai/net => ainet}/core/tensor/backend/CpuBackendTest.kt (100%) rename skainet-core/skainet-tensors/src/commonTest/kotlin/sk/{ai/net => ainet}/core/tensor/benchmark/BaselinePerformanceTest.kt (100%) rename skainet-core/skainet-tensors/src/commonTest/kotlin/sk/{ai/net => ainet}/core/tensor/benchmark/BenchmarkRunner.kt (100%) rename skainet-core/skainet-tensors/src/commonTest/kotlin/sk/{ai/net => ainet}/core/tensor/benchmark/TensorBenchmarkSuite.kt (100%) rename skainet-core/skainet-tensors/src/commonTest/kotlin/sk/{ai/net => ainet}/core/tensor/multiplication/EdgeCasesTest.kt (100%) rename skainet-core/skainet-tensors/src/commonTest/kotlin/sk/{ai/net => ainet}/core/tensor/multiplication/ElementwiseMultiplicationTest.kt (100%) rename skainet-core/skainet-tensors/src/commonTest/kotlin/sk/{ai/net => ainet}/core/tensor/multiplication/MLOperationsTest.kt (100%) rename skainet-core/skainet-tensors/src/commonTest/kotlin/sk/{ai/net => ainet}/core/tensor/multiplication/MatrixMultiplicationTest.kt (96%) diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/ComputeBackend.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/ComputeBackend.kt similarity index 100% rename from skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/ComputeBackend.kt rename to skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/ComputeBackend.kt diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/DType.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/DType.kt similarity index 100% rename from skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/DType.kt rename to skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/DType.kt diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Shape.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Shape.kt similarity index 100% rename from skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Shape.kt rename to skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Shape.kt diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Tensor.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Tensor.kt similarity index 99% rename from skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Tensor.kt rename to skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Tensor.kt index ee7881d0f..bbff47de7 100644 --- a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/Tensor.kt +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/Tensor.kt @@ -8,3 +8,4 @@ package sk.ai.net.core.tensor * allows accessing individual elements by their indices. */ public interface Tensor : TensorData, TensorOps> + diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorData.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorData.kt similarity index 100% rename from skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorData.kt rename to skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorData.kt diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorOps.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorOps.kt similarity index 100% rename from skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ai/net/core/tensor/TensorOps.kt rename to skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorOps.kt diff --git a/skainet-core/skainet-tensors/gradle.properties b/skainet-core/skainet-tensors/gradle.properties index a6f6cff10..cfbe3bfc8 100644 --- a/skainet-core/skainet-tensors/gradle.properties +++ b/skainet-core/skainet-tensors/gradle.properties @@ -1,2 +1,2 @@ -POM_ARTIFACT_ID=tensors +POM_ARTIFACT_ID=tensors-core POM_NAME=skainet core API \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ai/net/core/tensor/backend/CpuBackend.kt b/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt similarity index 85% rename from skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ai/net/core/tensor/backend/CpuBackend.kt rename to skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt index 067e80c3d..7664cc156 100644 --- a/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ai/net/core/tensor/backend/CpuBackend.kt +++ b/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt @@ -2,17 +2,82 @@ package sk.ai.net.core.tensor.backend import sk.ai.net.core.tensor.* +/** + * Convenient type alias for FP32 tensors with Float values. + * Use this instead of concrete implementations where possible for better abstraction. + */ +public typealias TensorFP32 = Tensor + +/** + * Extension function to print scalar tensors (represented as 1D tensor with single element). + * Returns a string representation of the scalar value. + */ +public fun TensorFP32.printScalar(): String { + require(shape.rank == 1 && shape[0] == 1) { "Tensor must be a scalar (1D with single element), got rank ${shape.rank} with shape ${shape.dimensions.contentToString()}" } + return this[0].toString() +} + +/** + * Extension function to print vector tensors (1D). + * Returns a string representation in the format [a, b, c, ...]. + */ +public fun TensorFP32.printVector(): String { + require(shape.rank == 1) { "Tensor must be a vector (1D), got rank ${shape.rank}" } + val elements = (0 until shape[0]).map { this[it] } + return "[${elements.joinToString(", ")}]" +} + +/** + * Extension function to print matrix tensors (2D). + * Returns a string representation with each row on a separate line. + */ +public fun TensorFP32.printMatrix(): String { + require(shape.rank == 2) { "Tensor must be a matrix (2D), got rank ${shape.rank}" } + val rows = shape[0] + val cols = shape[1] + val result = StringBuilder() + + result.append("[\n") + for (i in 0 until rows) { + result.append(" [") + for (j in 0 until cols) { + result.append(this[i, j]) + if (j < cols - 1) result.append(", ") + } + result.append("]") + if (i < rows - 1) result.append(",") + result.append("\n") + } + result.append("]") + + return result.toString() +} + +/** + * General extension function to print tensors of any dimension. + * Automatically selects the appropriate printing method based on tensor rank and shape. + */ +public fun TensorFP32.print(): String { + return when { + shape.rank == 1 && shape[0] == 1 -> printScalar() + shape.rank == 1 -> printVector() + shape.rank == 2 -> printMatrix() + else -> "Tensor(shape=${shape}, rank=${shape.rank}) - printing not supported for tensors with rank > 2" + } +} + + /** * A CPU-based tensor for FP32/Float values. * - * This tensor stores data on the CPU using simple FloatArray with NCHW row-major layout. + * This tensor stores data on the CPU using simple FloatArray with NCHW row-major layout. * It supports 1-4 dimensional tensors and delegates all operations to CpuBackend. */ public class CpuTensorFP32( override val shape: Shape, internal val data: FloatArray -) : Tensor { - +) : TensorFP32 { + init { require(data.size == shape.volume) { "Data size ${data.size} doesn't match shape volume ${shape.volume}" @@ -21,47 +86,47 @@ public class CpuTensorFP32( "Only 1-4 dimensional tensors are supported, got ${shape.rank}" } } - + override fun get(vararg indices: Int): Float { val index = shape.index(indices) return data[index] } - + // Delegate all operations to the CpuBackend private val backend = CpuBackend() - + override fun matmul(a: Tensor, b: Tensor): Tensor = backend.matmul(a, b) override fun scale(a: Tensor, scalar: Double): Tensor = backend.scale(a, scalar) override fun dot(a: Tensor, b: Tensor): Double = backend.dot(a, b) - + // Tensor-Tensor operations override fun Tensor.plus(other: Tensor): Tensor = with(backend) { this@plus.plus(other) } override fun Tensor.minus(other: Tensor): Tensor = with(backend) { this@minus.minus(other) } override fun Tensor.times(other: Tensor): Tensor = with(backend) { this@times.times(other) } override fun Tensor.div(other: Tensor): Tensor = with(backend) { this@div.div(other) } - + // Tensor-Scalar operations - delegate to backend override fun Tensor.plus(scalar: Int): Tensor = with(backend) { this@plus.plus(scalar) } override fun Tensor.minus(scalar: Int): Tensor = with(backend) { this@minus.minus(scalar) } override fun Tensor.times(scalar: Int): Tensor = with(backend) { this@times.times(scalar) } override fun Tensor.div(scalar: Int): Tensor = with(backend) { this@div.div(scalar) } - + override fun Tensor.plus(scalar: Float): Tensor = with(backend) { this@plus.plus(scalar) } override fun Tensor.minus(scalar: Float): Tensor = with(backend) { this@minus.minus(scalar) } override fun Tensor.times(scalar: Float): Tensor = with(backend) { this@times.times(scalar) } override fun Tensor.div(scalar: Float): Tensor = with(backend) { this@div.div(scalar) } - + override fun Tensor.plus(scalar: Double): Tensor = with(backend) { this@plus.plus(scalar) } override fun Tensor.minus(scalar: Double): Tensor = with(backend) { this@minus.minus(scalar) } override fun Tensor.times(scalar: Double): Tensor = with(backend) { this@times.times(scalar) } override fun Tensor.div(scalar: Double): Tensor = with(backend) { this@div.div(scalar) } - + // Scalar-Tensor operations - delegate to backend override fun Double.plus(t: Tensor): Tensor = with(backend) { this@plus.plus(t) } override fun Double.minus(t: Tensor): Tensor = with(backend) { this@minus.minus(t) } override fun Double.times(t: Tensor): Tensor = with(backend) { this@times.times(t) } override fun Double.div(t: Tensor): Tensor = with(backend) { this@div.div(t) } - + public companion object { /** * Creates a tensor from an array with the given shape. @@ -69,21 +134,21 @@ public class CpuTensorFP32( public fun fromArray(shape: Shape, data: FloatArray): CpuTensorFP32 { return CpuTensorFP32(shape, data) } - + /** * Creates a tensor filled with zeros. */ public fun zeros(shape: Shape): CpuTensorFP32 { return CpuTensorFP32(shape, FloatArray(shape.volume)) } - + /** * Creates a tensor filled with ones. */ public fun ones(shape: Shape): CpuTensorFP32 { return CpuTensorFP32(shape, FloatArray(shape.volume) { 1.0f }) } - + /** * Creates a tensor filled with a specific value. */ @@ -98,18 +163,18 @@ public class CpuTensorFP32( */ public class CpuBackend : ComputeBackend { override val name: String = "CPU" - + // Basic operations - implement the actual computation logic override fun matmul(a: Tensor, b: Tensor): Tensor { require(a is CpuTensorFP32 && b is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } require(a.shape.rank == 2 && b.shape.rank == 2) { "Matrix multiplication requires 2D tensors" } require(a.shape[1] == b.shape[0]) { "Matrix dimensions don't match for multiplication" } - + val rows = a.shape[0] val cols = b.shape[1] val inner = a.shape[1] val result = FloatArray(rows * cols) - + for (i in 0 until rows) { for (j in 0 until cols) { var sum = 0f @@ -119,164 +184,164 @@ public class CpuBackend : ComputeBackend { result[i * cols + j] = sum } } - + return CpuTensorFP32(Shape(rows, cols), result) } - + override fun scale(a: Tensor, scalar: Double): Tensor { require(a is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = a.data.map { it * scalar.toFloat() }.toFloatArray() return CpuTensorFP32(a.shape, result) } - + override fun dot(a: Tensor, b: Tensor): Double { require(a is CpuTensorFP32 && b is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } require(a.shape == b.shape) { "Tensors must have same shape for dot product" } - + var sum = 0.0 for (i in a.data.indices) { sum += a.data[i] * b.data[i] } return sum } - + // Tensor-Tensor operations - implement actual computation logic override fun Tensor.plus(other: Tensor): Tensor { require(this is CpuTensorFP32 && other is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } require(this.shape == other.shape) { "Tensors must have same shape for addition" } - + val result = FloatArray(this.data.size) for (i in this.data.indices) { result[i] = this.data[i] + other.data[i] } return CpuTensorFP32(this.shape, result) } - + override fun Tensor.minus(other: Tensor): Tensor { require(this is CpuTensorFP32 && other is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } require(this.shape == other.shape) { "Tensors must have same shape for subtraction" } - + val result = FloatArray(this.data.size) for (i in this.data.indices) { result[i] = this.data[i] - other.data[i] } return CpuTensorFP32(this.shape, result) } - + override fun Tensor.times(other: Tensor): Tensor { require(this is CpuTensorFP32 && other is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } require(this.shape == other.shape) { "Tensors must have same shape for element-wise multiplication" } - + val result = FloatArray(this.data.size) for (i in this.data.indices) { result[i] = this.data[i] * other.data[i] } return CpuTensorFP32(this.shape, result) } - + override fun Tensor.div(other: Tensor): Tensor { require(this is CpuTensorFP32 && other is CpuTensorFP32) { "Both tensors must be CpuTensorFP32" } require(this.shape == other.shape) { "Tensors must have same shape for element-wise division" } - + val result = FloatArray(this.data.size) for (i in this.data.indices) { result[i] = this.data[i] / other.data[i] } return CpuTensorFP32(this.shape, result) } - + // Tensor-Scalar operations override fun Tensor.plus(scalar: Int): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it + scalar }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + override fun Tensor.minus(scalar: Int): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it - scalar }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + override fun Tensor.times(scalar: Int): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it * scalar }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + override fun Tensor.div(scalar: Int): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it / scalar }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + override fun Tensor.plus(scalar: Float): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it + scalar }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + override fun Tensor.minus(scalar: Float): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it - scalar }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + override fun Tensor.times(scalar: Float): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it * scalar }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + override fun Tensor.div(scalar: Float): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it / scalar }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + override fun Tensor.plus(scalar: Double): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it + scalar.toFloat() }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + override fun Tensor.minus(scalar: Double): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it - scalar.toFloat() }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + override fun Tensor.times(scalar: Double): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it * scalar.toFloat() }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + override fun Tensor.div(scalar: Double): Tensor { require(this is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = this.data.map { it / scalar.toFloat() }.toFloatArray() return CpuTensorFP32(this.shape, result) } - + // Scalar-Tensor operations override fun Double.plus(t: Tensor): Tensor { require(t is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = t.data.map { this.toFloat() + it }.toFloatArray() return CpuTensorFP32(t.shape, result) } - + override fun Double.minus(t: Tensor): Tensor { require(t is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = t.data.map { this.toFloat() - it }.toFloatArray() return CpuTensorFP32(t.shape, result) } - + override fun Double.times(t: Tensor): Tensor { require(t is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = t.data.map { this.toFloat() * it }.toFloatArray() return CpuTensorFP32(t.shape, result) } - + override fun Double.div(t: Tensor): Tensor { require(t is CpuTensorFP32) { "Tensor must be CpuTensorFP32" } val result = t.data.map { this.toFloat() / it }.toFloatArray() diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt new file mode 100644 index 000000000..d2eb92bcd --- /dev/null +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt @@ -0,0 +1,125 @@ +package sk.ai.net.core.tensor + +import sk.ai.net.core.tensor.backend.* +import kotlin.test.* + +class PrintingTest { + + @Test + fun testScalarPrinting() { + // Test scalar (1D tensor with single element) + val scalar = CpuTensorFP32.fromArray(Shape(1), floatArrayOf(42.5f)) + + val scalarOutput = scalar.printScalar() + assertEquals("42.5", scalarOutput) + + val generalOutput = scalar.print() + assertEquals("42.5", generalOutput) + } + + @Test + fun testVectorPrinting() { + // Test vector (1D tensor) + val vector = CpuTensorFP32.fromArray(Shape(4), floatArrayOf(1.0f, 2.5f, -3.0f, 4.7f)) + + val vectorOutput = vector.printVector() + assertEquals("[1.0, 2.5, -3.0, 4.7]", vectorOutput) + + val generalOutput = vector.print() + assertEquals("[1.0, 2.5, -3.0, 4.7]", generalOutput) + } + + @Test + fun testMatrixPrinting() { + // Test matrix (2D tensor) + val matrix = CpuTensorFP32.fromArray( + Shape(2, 3), + floatArrayOf(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f) + ) + + val matrixOutput = matrix.printMatrix() + val expectedMatrix = """[ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0] +]""" + assertEquals(expectedMatrix, matrixOutput) + + val generalOutput = matrix.print() + assertEquals(expectedMatrix, generalOutput) + } + + @Test + fun testSingleElementMatrixPrinting() { + // Test 1x1 matrix + val singleMatrix = CpuTensorFP32.fromArray(Shape(1, 1), floatArrayOf(7.5f)) + + val matrixOutput = singleMatrix.printMatrix() + val expectedMatrix = """[ + [7.5] +]""" + assertEquals(expectedMatrix, matrixOutput) + + val generalOutput = singleMatrix.print() + assertEquals(expectedMatrix, generalOutput) + } + + @Test + fun testLargerMatrixPrinting() { + // Test larger matrix + val largerMatrix = CpuTensorFP32.fromArray( + Shape(3, 2), + floatArrayOf(1.1f, 2.2f, 3.3f, 4.4f, 5.5f, 6.6f) + ) + + val matrixOutput = largerMatrix.printMatrix() + val expectedMatrix = """[ + [1.1, 2.2], + [3.3, 4.4], + [5.5, 6.6] +]""" + assertEquals(expectedMatrix, matrixOutput) + } + + @Test + fun testWrongRankForScalar() { + // Test error handling for wrong tensor rank in printScalar + val vector = CpuTensorFP32.fromArray(Shape(3), floatArrayOf(1.0f, 2.0f, 3.0f)) + + assertFailsWith { + vector.printScalar() + } + } + + @Test + fun testWrongRankForVector() { + // Test error handling for wrong tensor rank in printVector + val matrix = CpuTensorFP32.fromArray(Shape(2, 2), floatArrayOf(1.0f, 2.0f, 3.0f, 4.0f)) + + assertFailsWith { + matrix.printVector() + } + } + + @Test + fun testWrongRankForMatrix() { + // Test error handling for wrong tensor rank in printMatrix + val vector = CpuTensorFP32.fromArray(Shape(3), floatArrayOf(1.0f, 2.0f, 3.0f)) + + assertFailsWith { + vector.printMatrix() + } + } + + @Test + fun testHigherDimensionTensorPrinting() { + // Test that higher-dimensional tensors show appropriate message + val tensor3D = CpuTensorFP32.fromArray( + Shape(2, 2, 2), + FloatArray(8) { it.toFloat() } + ) + + val output = tensor3D.print() + assertTrue(output.contains("printing not supported for tensors with rank > 2")) + assertTrue(output.contains("rank=3")) + } +} \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/TestUtils.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/TestUtils.kt similarity index 100% rename from skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/TestUtils.kt rename to skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/TestUtils.kt diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/backend/CpuBackendTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/backend/CpuBackendTest.kt similarity index 100% rename from skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/backend/CpuBackendTest.kt rename to skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/backend/CpuBackendTest.kt diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BaselinePerformanceTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BaselinePerformanceTest.kt similarity index 100% rename from skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BaselinePerformanceTest.kt rename to skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BaselinePerformanceTest.kt diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BenchmarkRunner.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BenchmarkRunner.kt similarity index 100% rename from skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/BenchmarkRunner.kt rename to skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BenchmarkRunner.kt diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/TensorBenchmarkSuite.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/TensorBenchmarkSuite.kt similarity index 100% rename from skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/benchmark/TensorBenchmarkSuite.kt rename to skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/TensorBenchmarkSuite.kt diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/EdgeCasesTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/EdgeCasesTest.kt similarity index 100% rename from skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/EdgeCasesTest.kt rename to skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/EdgeCasesTest.kt diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/ElementwiseMultiplicationTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/ElementwiseMultiplicationTest.kt similarity index 100% rename from skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/ElementwiseMultiplicationTest.kt rename to skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/ElementwiseMultiplicationTest.kt diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MLOperationsTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MLOperationsTest.kt similarity index 100% rename from skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MLOperationsTest.kt rename to skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MLOperationsTest.kt diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MatrixMultiplicationTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MatrixMultiplicationTest.kt similarity index 96% rename from skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MatrixMultiplicationTest.kt rename to skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MatrixMultiplicationTest.kt index 19a266b1f..cf15dcfe2 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ai/net/core/tensor/multiplication/MatrixMultiplicationTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MatrixMultiplicationTest.kt @@ -15,8 +15,8 @@ class MatrixMultiplicationTest { val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) // 2x3 matrix val data2 = floatArrayOf(7f, 8f, 9f, 10f, 11f, 12f) // 3x2 matrix - val tensor1 = CpuTensorFP32.fromArray(shape1, data1) - val tensor2 = CpuTensorFP32.fromArray(shape2, data2) + val tensor1: TensorFP32 = CpuTensorFP32.fromArray(shape1, data1) + val tensor2: TensorFP32 = CpuTensorFP32.fromArray(shape2, data2) val result = backend.matmul(tensor1, tensor2) @@ -35,8 +35,8 @@ class MatrixMultiplicationTest { val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f) val data2 = floatArrayOf(9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f) - val tensor1 = CpuTensorFP32.fromArray(shape, data1) - val tensor2 = CpuTensorFP32.fromArray(shape, data2) + val tensor1: TensorFP32 = CpuTensorFP32.fromArray(shape, data1) + val tensor2: TensorFP32 = CpuTensorFP32.fromArray(shape, data2) val result = backend.matmul(tensor1, tensor2) From 5f90c836ca211f3d6e3108597117da1b9f52d73b Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 10:54:20 +0200 Subject: [PATCH 07/16] Add getting started docs as a new antora module Related-To: #101 --- docs/antora.yml | 1 + docs/modules/getting-started/nav.adoc | 10 + .../getting-started/pages/basic-tensors.adoc | 328 +++++++++ .../pages/data-processing.adoc | 670 ++++++++++++++++++ docs/modules/getting-started/pages/index.adoc | 87 +++ .../getting-started/pages/installation.adoc | 187 +++++ .../pages/matrix-operations.adoc | 362 ++++++++++ .../pages/neural-network-basics.adoc | 422 +++++++++++ .../pages/performance-tips.adoc | 129 ++++ 9 files changed, 2196 insertions(+) create mode 100644 docs/modules/getting-started/nav.adoc create mode 100644 docs/modules/getting-started/pages/basic-tensors.adoc create mode 100644 docs/modules/getting-started/pages/data-processing.adoc create mode 100644 docs/modules/getting-started/pages/index.adoc create mode 100644 docs/modules/getting-started/pages/installation.adoc create mode 100644 docs/modules/getting-started/pages/matrix-operations.adoc create mode 100644 docs/modules/getting-started/pages/neural-network-basics.adoc create mode 100644 docs/modules/getting-started/pages/performance-tips.adoc diff --git a/docs/antora.yml b/docs/antora.yml index e843664f9..5f2cbff9b 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -2,5 +2,6 @@ name: skainet title: Skainet Deep Learning Framework version: '1.0' nav: +- modules/getting-started/nav.adoc - modules/ROOT/nav.adoc - modules/arc42/nav.adoc \ No newline at end of file diff --git a/docs/modules/getting-started/nav.adoc b/docs/modules/getting-started/nav.adoc new file mode 100644 index 000000000..d17b2ed4b --- /dev/null +++ b/docs/modules/getting-started/nav.adoc @@ -0,0 +1,10 @@ += Getting Started with SKaiNET + +* xref:index.adoc[Introduction] +* xref:installation.adoc[Installation & Setup] +* xref:basic-tensors.adoc[Basic Tensor Operations] +* xref:matrix-operations.adoc[Matrix Operations] +* xref:linear-algebra.adoc[Linear Algebra Examples] +* xref:neural-network-basics.adoc[Neural Network Basics] +* xref:data-processing.adoc[Data Processing Use Cases] +* xref:performance-tips.adoc[Performance Tips] \ No newline at end of file diff --git a/docs/modules/getting-started/pages/basic-tensors.adoc b/docs/modules/getting-started/pages/basic-tensors.adoc new file mode 100644 index 000000000..3dbdcdc7f --- /dev/null +++ b/docs/modules/getting-started/pages/basic-tensors.adoc @@ -0,0 +1,328 @@ += Basic Tensor Operations +:toc: left +:toclevels: 3 +:sectanchors: +:sectlinks: + +Learn the fundamentals of working with tensors in SKaiNET through practical examples. + +== What are Tensors? + +Tensors are multi-dimensional arrays that generalize scalars, vectors, and matrices: + +* **Scalar** (0D tensor): A single number +* **Vector** (1D tensor): An array of numbers +* **Matrix** (2D tensor): A rectangular array of numbers +* **Higher-dimensional tensors**: 3D, 4D, and beyond + +== Creating Tensors + +=== From Arrays + +The most common way to create tensors is from data arrays: + +[source,kotlin] +---- +// Scalar (0D tensor) +val scalar: TensorFP32 = CpuTensorFP32.fromArray( + Shape(), + floatArrayOf(42.0f) +) + +// Vector (1D tensor) +val vector: TensorFP32 = CpuTensorFP32.fromArray( + Shape(4), + floatArrayOf(1.0f, 2.0f, 3.0f, 4.0f) +) + +// Matrix (2D tensor) +val matrix: TensorFP32 = CpuTensorFP32.fromArray( + Shape(2, 3), + floatArrayOf(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f) +) + +// 3D tensor (e.g., RGB image: height × width × channels) +val image: TensorFP32 = CpuTensorFP32.fromArray( + Shape(32, 32, 3), + FloatArray(32 * 32 * 3) { it.toFloat() } +) +---- + +=== Understanding Shapes + +The `Shape` class defines tensor dimensions: + +[source,kotlin] +---- +val shape = Shape(2, 3, 4) // 2×3×4 tensor +println("Dimensions: ${shape.dimensions}") // [2, 3, 4] +println("Total elements: ${shape.size}") // 24 +println("Rank: ${shape.rank}") // 3 +---- + +== Accessing Tensor Elements + +Use the indexing operator to access individual elements: + +[source,kotlin] +---- +val matrix = CpuTensorFP32.fromArray( + Shape(3, 3), + floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f) +) + +// Access elements +println("Element at [0,0]: ${matrix[0, 0]}") // 1.0 +println("Element at [1,2]: ${matrix[1, 2]}") // 6.0 +println("Element at [2,1]: ${matrix[2, 1]}") // 8.0 +---- + +== Printing Tensors + +SKaiNET provides convenient extension functions to print tensors in a readable format: + +[source,kotlin] +---- +// Scalar (represented as 1D tensor with single element) +val scalar = CpuTensorFP32.fromArray(Shape(1), floatArrayOf(42.5f)) +println("Scalar: ${scalar.print()}") +// Output: Scalar: 42.5 + +// Vector +val vector = CpuTensorFP32.fromArray(Shape(4), floatArrayOf(1f, 2f, 3f, 4f)) +println("Vector: ${vector.print()}") +// Output: Vector: [1.0, 2.0, 3.0, 4.0] + +// Matrix +val matrix = CpuTensorFP32.fromArray( + Shape(2, 3), + floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) +) +println("Matrix:\n${matrix.print()}") +// Output: Matrix: +// [ +// [1.0, 2.0, 3.0], +// [4.0, 5.0, 6.0] +// ] + +// You can also use specific printing functions +println("Vector using printVector(): ${vector.printVector()}") +println("Matrix using printMatrix():\n${matrix.printMatrix()}") +---- + +== Basic Arithmetic Operations + +=== Element-wise Operations + +Perform operations on corresponding elements of tensors: + +[source,kotlin] +---- +val backend = CpuBackend() + +val a = CpuTensorFP32.fromArray(Shape(2, 2), floatArrayOf(1f, 2f, 3f, 4f)) +val b = CpuTensorFP32.fromArray(Shape(2, 2), floatArrayOf(5f, 6f, 7f, 8f)) + +// Addition +val sum = with(backend) { a + b } +// Result: [6, 8, 10, 12] + +// Subtraction +val diff = with(backend) { a - b } +// Result: [-4, -4, -4, -4] + +// Element-wise multiplication +val product = with(backend) { a * b } +// Result: [5, 12, 21, 32] + +// Element-wise division +val quotient = with(backend) { a / b } +// Result: [0.2, 0.33, 0.43, 0.5] +---- + +=== Scalar Operations + +Apply operations with single values: + +[source,kotlin] +---- +val tensor = CpuTensorFP32.fromArray(Shape(2, 2), floatArrayOf(1f, 2f, 3f, 4f)) + +val backend = CpuBackend() +with(backend) { + val scaled = tensor * 2.0f // [2, 4, 6, 8] + val shifted = tensor + 10.0f // [11, 12, 13, 14] + val normalized = tensor / 4.0f // [0.25, 0.5, 0.75, 1.0] +} +---- + +== Practical Example: Data Normalization + +A common preprocessing step in machine learning: + +[source,kotlin] +---- +fun normalizeData(data: CpuTensorFP32, backend: CpuBackend): CpuTensorFP32 { + // Find min and max values (simplified approach) + var min = Float.MAX_VALUE + var max = Float.MIN_VALUE + + val shape = data.shape + for (i in 0 until shape.dimensions[0]) { + for (j in 0 until shape.dimensions[1]) { + val value = data[i, j] + if (value < min) min = value + if (value > max) max = value + } + } + + // Normalize to [0, 1] range + val range = max - min + return with(backend) { + (data - min) / range + } +} + +// Example usage +val rawData = CpuTensorFP32.fromArray( + Shape(3, 4), + floatArrayOf(10f, 20f, 30f, 40f, 50f, 60f, 70f, 80f, 90f, 100f, 110f, 120f) +) + +val backend = CpuBackend() +val normalized = normalizeData(rawData, backend) + +println("Original range: 10-120") +println("Normalized range: 0-1") +---- + +== Practical Example: Distance Calculations + +Calculate Euclidean distance between data points: + +[source,kotlin] +---- +fun euclideanDistance( + point1: CpuTensorFP32, + point2: CpuTensorFP32, + backend: CpuBackend +): Float { + // Calculate difference + val diff = with(backend) { point1 - point2 } + + // Square the differences + val squared = with(backend) { diff * diff } + + // Sum all elements (manual implementation for demonstration) + var sum = 0.0f + for (i in 0 until squared.shape.dimensions[0]) { + sum += squared[i] + } + + return kotlin.math.sqrt(sum) +} + +// Example: Calculate distance between two 3D points +val point1 = CpuTensorFP32.fromArray(Shape(3), floatArrayOf(1f, 2f, 3f)) +val point2 = CpuTensorFP32.fromArray(Shape(3), floatArrayOf(4f, 5f, 6f)) + +val backend = CpuBackend() +val distance = euclideanDistance(point1, point2, backend) +println("Distance: $distance") // Should be ~5.196 +---- + +== Practical Example: Moving Average + +Smooth time series data using a simple moving average: + +[source,kotlin] +---- +fun movingAverage( + timeSeries: CpuTensorFP32, + windowSize: Int, + backend: CpuBackend +): CpuTensorFP32 { + val length = timeSeries.shape.dimensions[0] + val result = FloatArray(length - windowSize + 1) + + for (i in 0..length - windowSize) { + var sum = 0.0f + for (j in i until i + windowSize) { + sum += timeSeries[j] + } + result[i - 0] = sum / windowSize + } + + return CpuTensorFP32.fromArray(Shape(result.size), result) +} + +// Example: Smooth stock price data +val prices = CpuTensorFP32.fromArray( + Shape(10), + floatArrayOf(100f, 102f, 98f, 105f, 103f, 107f, 104f, 109f, 106f, 111f) +) + +val backend = CpuBackend() +val smoothed = movingAverage(prices, 3, backend) + +println("Original prices: ${(0 until 10).map { prices[it] }}") +println("3-day moving average: ${(0 until smoothed.shape.dimensions[0]).map { smoothed[it] }}") +---- + +== Working with Different Data Types + +SKaiNET supports different numeric precisions: + +[source,kotlin] +---- +// 32-bit floating point (most common) +val fp32 = CpuTensorFP32.fromArray(Shape(2, 2), floatArrayOf(1f, 2f, 3f, 4f)) + +// You can also work with different precisions as needed +// The type system ensures type safety +---- + +== Common Patterns and Best Practices + +=== Use Backend Context + +Always use the backend for operations: + +[source,kotlin] +---- +val backend = CpuBackend() + +// Good: Use backend context +with(backend) { + val result = tensor1 + tensor2 +} + +// Also good: Explicit backend calls +val result = backend.matmul(tensor1, tensor2) +---- + +=== Shape Compatibility + +Ensure tensor shapes are compatible for operations: + +[source,kotlin] +---- +val a = CpuTensorFP32.fromArray(Shape(2, 3), floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f)) +val b = CpuTensorFP32.fromArray(Shape(2, 3), floatArrayOf(7f, 8f, 9f, 10f, 11f, 12f)) + +// This works - same shapes +val sum = with(backend) { a + b } + +// This won't work - incompatible shapes +// val c = CpuTensorFP32.fromArray(Shape(3, 2), floatArrayOf(...)) +// val invalid = with(backend) { a + c } // Error! +---- + +== Next Steps + +Now that you understand basic tensor operations, let's explore more advanced operations in xref:matrix-operations.adoc[Matrix Operations]. + +[TIP] +==== +Practice these examples by modifying the data and operations. Understanding how tensors behave with different shapes and operations is crucial for more advanced applications. +==== \ No newline at end of file diff --git a/docs/modules/getting-started/pages/data-processing.adoc b/docs/modules/getting-started/pages/data-processing.adoc new file mode 100644 index 000000000..02e879cce --- /dev/null +++ b/docs/modules/getting-started/pages/data-processing.adoc @@ -0,0 +1,670 @@ += Data Processing Use Cases +:toc: left +:toclevels: 3 +:sectanchors: +:sectlinks: + +Explore practical data processing scenarios using SKaiNET for real-world applications in machine learning and scientific computing. + +== Image Processing + +=== Image Filtering and Convolutions + +Apply filters to images for feature extraction and preprocessing: + +[source,kotlin] +---- +// Create common image processing kernels +object ImageKernels { + + fun gaussianBlur(): CpuTensorFP32 { + return CpuTensorFP32.fromArray( + Shape(3, 3), + floatArrayOf( + 1f/16f, 2f/16f, 1f/16f, + 2f/16f, 4f/16f, 2f/16f, + 1f/16f, 2f/16f, 1f/16f + ) + ) + } + + fun edgeDetectionSobel(): CpuTensorFP32 { + return CpuTensorFP32.fromArray( + Shape(3, 3), + floatArrayOf( + -1f, 0f, 1f, + -2f, 0f, 2f, + -1f, 0f, 1f + ) + ) + } + + fun sharpen(): CpuTensorFP32 { + return CpuTensorFP32.fromArray( + Shape(3, 3), + floatArrayOf( + 0f, -1f, 0f, + -1f, 5f, -1f, + 0f, -1f, 0f + ) + ) + } +} + +// Apply convolution to image +fun applyConvolution( + image: CpuTensorFP32, + kernel: CpuTensorFP32, + backend: CpuBackend +): CpuTensorFP32 { + val height = image.shape.dimensions[0] + val width = image.shape.dimensions[1] + val resultHeight = height - 2 + val resultWidth = width - 2 + + val result = FloatArray(resultHeight * resultWidth) + + for (i in 0 until resultHeight) { + for (j in 0 until resultWidth) { + var sum = 0f + + // Apply kernel + for (ki in 0 until 3) { + for (kj in 0 until 3) { + sum += image[i + ki, j + kj] * kernel[ki, kj] + } + } + + result[i * resultWidth + j] = sum + } + } + + return CpuTensorFP32.fromArray(Shape(resultHeight, resultWidth), result) +} + +// Example: Process a sample image +val backend = CpuBackend() + +// Create a sample 8x8 grayscale image +val sampleImage = CpuTensorFP32.fromArray( + Shape(8, 8), + FloatArray(64) { i -> + val row = i / 8 + val col = i % 8 + // Create a simple pattern + if (row < 4 && col < 4) 255f else 0f + } +) + +// Apply different filters +val blurred = applyConvolution(sampleImage, ImageKernels.gaussianBlur(), backend) +val edges = applyConvolution(sampleImage, ImageKernels.edgeDetectionSobel(), backend) +val sharpened = applyConvolution(sampleImage, ImageKernels.sharpen(), backend) + +println("Original image shape: ${sampleImage.shape}") +println("Processed image shape: ${blurred.shape}") +---- + +=== Image Normalization and Preprocessing + +Prepare images for machine learning models: + +[source,kotlin] +---- +class ImagePreprocessor(val backend: CpuBackend) { + + // Normalize image to [0, 1] range + fun normalizeToUnitRange(image: CpuTensorFP32): CpuTensorFP32 { + val shape = image.shape + + // Find min and max values + var minVal = Float.MAX_VALUE + var maxVal = Float.MIN_VALUE + + for (i in 0 until shape.dimensions[0]) { + for (j in 0 until shape.dimensions[1]) { + val value = image[i, j] + if (value < minVal) minVal = value + if (value > maxVal) maxVal = value + } + } + + val range = maxVal - minVal + + return if (range > 0f) { + with(backend) { + (image - minVal) / range + } + } else { + image + } + } + + // Standardize image (zero mean, unit variance) + fun standardize(image: CpuTensorFP32): CpuTensorFP32 { + val shape = image.shape + val totalPixels = shape.size + + // Calculate mean + var sum = 0f + for (i in 0 until shape.dimensions[0]) { + for (j in 0 until shape.dimensions[1]) { + sum += image[i, j] + } + } + val mean = sum / totalPixels + + // Calculate standard deviation + var varianceSum = 0f + for (i in 0 until shape.dimensions[0]) { + for (j in 0 until shape.dimensions[1]) { + val diff = image[i, j] - mean + varianceSum += diff * diff + } + } + val stdDev = kotlin.math.sqrt(varianceSum / totalPixels) + + return if (stdDev > 0f) { + with(backend) { + (image - mean) / stdDev + } + } else { + with(backend) { + image - mean + } + } + } + + // Resize image using nearest neighbor (simplified) + fun resize(image: CpuTensorFP32, newHeight: Int, newWidth: Int): CpuTensorFP32 { + val oldHeight = image.shape.dimensions[0] + val oldWidth = image.shape.dimensions[1] + + val result = FloatArray(newHeight * newWidth) { i -> + val newRow = i / newWidth + val newCol = i % newWidth + + val oldRow = (newRow * oldHeight) / newHeight + val oldCol = (newCol * oldWidth) / newWidth + + image[oldRow, oldCol] + } + + return CpuTensorFP32.fromArray(Shape(newHeight, newWidth), result) + } +} + +// Example usage +val preprocessor = ImagePreprocessor(backend) +val rawImage = CpuTensorFP32.fromArray( + Shape(4, 4), + floatArrayOf( + 100f, 120f, 110f, 130f, + 90f, 140f, 125f, 135f, + 95f, 115f, 105f, 145f, + 85f, 155f, 150f, 160f + ) +) + +val normalized = preprocessor.normalizeToUnitRange(rawImage) +val standardized = preprocessor.standardize(rawImage) +val resized = preprocessor.resize(rawImage, 8, 8) + +println("Original shape: ${rawImage.shape}") +println("Resized shape: ${resized.shape}") +---- + +== Time Series Processing + +=== Signal Filtering and Smoothing + +Process temporal data for analysis and prediction: + +[source,kotlin] +---- +class TimeSeriesProcessor(val backend: CpuBackend) { + + // Simple moving average filter + fun movingAverage(series: CpuTensorFP32, windowSize: Int): CpuTensorFP32 { + val length = series.shape.dimensions[0] + val resultLength = length - windowSize + 1 + val result = FloatArray(resultLength) + + for (i in 0 until resultLength) { + var sum = 0f + for (j in 0 until windowSize) { + sum += series[i + j] + } + result[i] = sum / windowSize + } + + return CpuTensorFP32.fromArray(Shape(resultLength), result) + } + + // Exponential moving average + fun exponentialMovingAverage(series: CpuTensorFP32, alpha: Float): CpuTensorFP32 { + val length = series.shape.dimensions[0] + val result = FloatArray(length) + + result[0] = series[0] + + for (i in 1 until length) { + result[i] = alpha * series[i] + (1 - alpha) * result[i - 1] + } + + return CpuTensorFP32.fromArray(Shape(length), result) + } + + // Difference the series (for stationarity) + fun difference(series: CpuTensorFP32, order: Int = 1): CpuTensorFP32 { + var current = series + + repeat(order) { + val length = current.shape.dimensions[0] + val result = FloatArray(length - 1) + + for (i in 0 until length - 1) { + result[i] = current[i + 1] - current[i] + } + + current = CpuTensorFP32.fromArray(Shape(result.size), result) + } + + return current + } + + // Create lagged features for prediction + fun createLaggedFeatures(series: CpuTensorFP32, numLags: Int): CpuTensorFP32 { + val length = series.shape.dimensions[0] + val resultLength = length - numLags + val result = FloatArray(resultLength * numLags) + + for (i in 0 until resultLength) { + for (lag in 0 until numLags) { + result[i * numLags + lag] = series[i + lag] + } + } + + return CpuTensorFP32.fromArray(Shape(resultLength, numLags), result) + } +} + +// Example: Stock price analysis +val processor = TimeSeriesProcessor(backend) + +// Simulate stock prices +val stockPrices = CpuTensorFP32.fromArray( + Shape(50), + FloatArray(50) { i -> + 100f + 10f * kotlin.math.sin(i * 0.2f) + kotlin.random.Random.nextGaussian().toFloat() * 2f + } +) + +val smoothed = processor.movingAverage(stockPrices, 5) +val ema = processor.exponentialMovingAverage(stockPrices, 0.3f) +val differenced = processor.difference(stockPrices) +val laggedFeatures = processor.createLaggedFeatures(stockPrices, 3) + +println("Original series length: ${stockPrices.shape.dimensions[0]}") +println("Smoothed series length: ${smoothed.shape.dimensions[0]}") +println("Lagged features shape: ${laggedFeatures.shape}") +---- + +== Natural Language Processing (Text Processing) + +=== Text Vectorization + +Convert text to numerical representations: + +[source,kotlin] +---- +class TextVectorizer { + + // Simple bag-of-words representation + fun createVocabulary(texts: List): Map { + val vocabulary = mutableMapOf() + var index = 0 + + texts.forEach { text -> + text.lowercase() + .split(Regex("\\s+")) + .forEach { word -> + if (word.isNotEmpty() && !vocabulary.containsKey(word)) { + vocabulary[word] = index++ + } + } + } + + return vocabulary + } + + // Convert text to bag-of-words vector + fun textToBagOfWords(text: String, vocabulary: Map): CpuTensorFP32 { + val vector = FloatArray(vocabulary.size) { 0f } + + text.lowercase() + .split(Regex("\\s+")) + .forEach { word -> + vocabulary[word]?.let { index -> + vector[index] += 1f + } + } + + return CpuTensorFP32.fromArray(Shape(vocabulary.size), vector) + } + + // TF-IDF calculation (simplified) + fun calculateTfIdf( + texts: List, + vocabulary: Map + ): List { + val numDocs = texts.size + + // Calculate document frequency for each word + val documentFreq = FloatArray(vocabulary.size) { 0f } + + texts.forEach { text -> + val wordsInDoc = text.lowercase().split(Regex("\\s+")).toSet() + wordsInDoc.forEach { word -> + vocabulary[word]?.let { index -> + documentFreq[index] += 1f + } + } + } + + return texts.map { text -> + val words = text.lowercase().split(Regex("\\s+")) + val termFreq = FloatArray(vocabulary.size) { 0f } + + // Calculate term frequency + words.forEach { word -> + vocabulary[word]?.let { index -> + termFreq[index] += 1f + } + } + + // Calculate TF-IDF + val tfidf = FloatArray(vocabulary.size) { i -> + val tf = termFreq[i] / words.size + val idf = kotlin.math.ln(numDocs.toFloat() / (documentFreq[i] + 1f)) + tf * idf + } + + CpuTensorFP32.fromArray(Shape(vocabulary.size), tfidf) + } + } +} + +// Example: Document classification preprocessing +val vectorizer = TextVectorizer() + +val documents = listOf( + "machine learning is powerful", + "deep learning neural networks", + "artificial intelligence algorithms", + "data science and analytics", + "computer vision applications" +) + +val vocabulary = vectorizer.createVocabulary(documents) +val bagOfWordsVectors = documents.map { doc -> + vectorizer.textToBagOfWords(doc, vocabulary) +} +val tfidfVectors = vectorizer.calculateTfIdf(documents, vocabulary) + +println("Vocabulary size: ${vocabulary.size}") +println("Document vector shape: ${bagOfWordsVectors[0].shape}") +println("Sample vocabulary: ${vocabulary.keys.take(10)}") +---- + +== Batch Data Processing + +=== Efficient Batch Operations + +Process large datasets efficiently using tensor batching: + +[source,kotlin] +---- +class BatchProcessor(val backend: CpuBackend) { + + // Process data in batches + fun processBatches( + data: List, + batchSize: Int, + processor: (List) -> CpuTensorFP32 + ): List { + return data.chunked(batchSize).map { batch -> + processor(batch) + } + } + + // Stack multiple tensors into a batch tensor + fun stackTensors(tensors: List): CpuTensorFP32 { + if (tensors.isEmpty()) { + throw IllegalArgumentException("Cannot stack empty list of tensors") + } + + val batchSize = tensors.size + val sampleShape = tensors[0].shape + val batchShape = Shape(batchSize, *sampleShape.dimensions) + + val batchData = FloatArray(batchSize * sampleShape.size) { i -> + val sampleIndex = i / sampleShape.size + val elementIndex = i % sampleShape.size + + // Convert flat index to coordinates for the sample + val coords = IntArray(sampleShape.rank) + var remaining = elementIndex + for (j in sampleShape.rank - 1 downTo 0) { + coords[j] = remaining % sampleShape.dimensions[j] + remaining /= sampleShape.dimensions[j] + } + + tensors[sampleIndex][*coords] + } + + return CpuTensorFP32.fromArray(batchShape, batchData) + } + + // Apply normalization to entire batch + fun batchNormalize(batch: CpuTensorFP32): CpuTensorFP32 { + val batchSize = batch.shape.dimensions[0] + val featureSize = batch.shape.dimensions[1] + + // Calculate mean and std across batch + val means = FloatArray(featureSize) { feature -> + var sum = 0f + for (sample in 0 until batchSize) { + sum += batch[sample, feature] + } + sum / batchSize + } + + val stds = FloatArray(featureSize) { feature -> + var varianceSum = 0f + for (sample in 0 until batchSize) { + val diff = batch[sample, feature] - means[feature] + varianceSum += diff * diff + } + kotlin.math.sqrt(varianceSum / batchSize) + } + + // Normalize + val normalized = FloatArray(batchSize * featureSize) { i -> + val sample = i / featureSize + val feature = i % featureSize + val value = batch[sample, feature] + if (stds[feature] > 0f) { + (value - means[feature]) / stds[feature] + } else { + value - means[feature] + } + } + + return CpuTensorFP32.fromArray(batch.shape, normalized) + } +} + +// Example: Process dataset in batches +val processor = BatchProcessor(backend) + +// Simulate a dataset of feature vectors +val dataset = List(100) { i -> + CpuTensorFP32.fromArray( + Shape(5), + FloatArray(5) { kotlin.random.Random.nextGaussian().toFloat() + i * 0.1f } + ) +} + +// Stack into batches and process +val batchSize = 16 +val batches = dataset.chunked(batchSize).map { batch -> + processor.stackTensors(batch) +} + +println("Dataset size: ${dataset.size}") +println("Number of batches: ${batches.size}") +println("Batch shape: ${batches[0].shape}") + +// Apply batch normalization +val normalizedBatches = batches.map { batch -> + processor.batchNormalize(batch) +} + +println("Normalized batch shape: ${normalizedBatches[0].shape}") +---- + +== Data Augmentation + +=== Image Data Augmentation + +Increase dataset size and improve model robustness: + +[source,kotlin] +---- +class DataAugmenter(val backend: CpuBackend) { + + // Add random noise to image + fun addNoise(image: CpuTensorFP32, noiseLevel: Float): CpuTensorFP32 { + val shape = image.shape + val noisy = FloatArray(shape.size) { i -> + val coords = IntArray(shape.rank) + var remaining = i + for (j in shape.rank - 1 downTo 0) { + coords[j] = remaining % shape.dimensions[j] + remaining /= shape.dimensions[j] + } + + val originalValue = image[*coords] + val noise = kotlin.random.Random.nextGaussian().toFloat() * noiseLevel + originalValue + noise + } + + return CpuTensorFP32.fromArray(shape, noisy) + } + + // Horizontal flip + fun horizontalFlip(image: CpuTensorFP32): CpuTensorFP32 { + val height = image.shape.dimensions[0] + val width = image.shape.dimensions[1] + + val flipped = FloatArray(height * width) { i -> + val row = i / width + val col = i % width + val flippedCol = width - 1 - col + image[row, flippedCol] + } + + return CpuTensorFP32.fromArray(image.shape, flipped) + } + + // Brightness adjustment + fun adjustBrightness(image: CpuTensorFP32, factor: Float): CpuTensorFP32 { + return with(backend) { + image * factor + } + } + + // Generate multiple augmented versions + fun augmentImage(image: CpuTensorFP32, numAugmentations: Int): List { + val augmented = mutableListOf() + + repeat(numAugmentations) { + var current = image + + // Random horizontal flip + if (kotlin.random.Random.nextBoolean()) { + current = horizontalFlip(current) + } + + // Random brightness adjustment + val brightnessFactor = 0.8f + kotlin.random.Random.nextFloat() * 0.4f + current = adjustBrightness(current, brightnessFactor) + + // Random noise + if (kotlin.random.Random.nextFloat() < 0.3f) { + current = addNoise(current, 0.05f) + } + + augmented.add(current) + } + + return augmented + } +} + +// Example: Augment training data +val augmenter = DataAugmenter(backend) + +val originalImage = CpuTensorFP32.fromArray( + Shape(8, 8), + FloatArray(64) { i -> + val row = i / 8 + val col = i % 8 + if ((row + col) % 2 == 0) 1f else 0f + } +) + +val augmentedImages = augmenter.augmentImage(originalImage, 5) + +println("Original image shape: ${originalImage.shape}") +println("Generated ${augmentedImages.size} augmented versions") +---- + +== Performance Tips for Data Processing + +=== Memory Efficient Processing + +[source,kotlin] +---- +// Process data in chunks to manage memory +fun processLargeDataset( + dataLoader: () -> List, + chunkSize: Int, + processor: (CpuTensorFP32) -> CpuTensorFP32 +): List { + val results = mutableListOf() + val data = dataLoader() + + data.chunked(chunkSize).forEach { chunk -> + // Process chunk and collect results + chunk.forEach { sample -> + results.add(processor(sample)) + } + + // Optional: trigger garbage collection between chunks + System.gc() + } + + return results +} +---- + +== Next Steps + +With data processing mastered, you now have all the tools needed to build complete machine learning pipelines with SKaiNET. Consider exploring xref:performance-tips.adoc[Performance Tips] for optimizing your implementations. + +[TIP] +==== +Data processing is often the most time-consuming part of machine learning projects. Efficient preprocessing and augmentation pipelines can significantly improve both training speed and model performance. +==== \ No newline at end of file diff --git a/docs/modules/getting-started/pages/index.adoc b/docs/modules/getting-started/pages/index.adoc new file mode 100644 index 000000000..3bdda973f --- /dev/null +++ b/docs/modules/getting-started/pages/index.adoc @@ -0,0 +1,87 @@ += Getting Started with SKaiNET +:toc: left +:toclevels: 3 +:sectanchors: +:sectlinks: + +Welcome to SKaiNET, a modern Kotlin multiplatform tensor computation library designed for machine learning and scientific computing. This guide will help you get started with practical, real-world examples. + +== What is SKaiNET? + +SKaiNET is a high-performance tensor computation framework that provides: + +* **Multi-dimensional tensor operations** - Work with scalars, vectors, matrices, and higher-dimensional arrays +* **Comprehensive mathematical operations** - Element-wise operations, matrix multiplication, linear algebra +* **Kotlin multiplatform support** - Run on JVM, Android, iOS, and native platforms +* **Clean, intuitive API** - Operator overloading makes tensor math feel natural +* **Performance-focused** - Optimized CPU backend with room for GPU acceleration + +== Real-World Use Cases + +This guide covers practical scenarios where SKaiNET excels: + +=== 🤖 Machine Learning Foundations +* Neural network forward passes +* Gradient computations and backpropagation +* Feature transformations and preprocessing + +== Quick Example + +Here's a taste of what SKaiNET looks like in action: + +[source,kotlin] +---- +import sk.ainet.core.tensor.* +import sk.ainet.core.tensor.backend.* + +// Create a CPU backend +val backend = CpuBackend() + +// Create some tensors +val weights = CpuTensorFP32.fromArray( + Shape(3, 2), + floatArrayOf(0.5f, -0.3f, 0.8f, 0.2f, -0.1f, 0.9f) +) + +val input = CpuTensorFP32.fromArray( + Shape(2, 1), + floatArrayOf(1.0f, 0.5f) +) + +// Matrix multiplication - the foundation of neural networks +val output = backend.matmul(weights, input) + +// Element-wise operations with natural syntax +val scaled = backend.scale(output, 2.0) +val biased = scaled + 0.1f + +println("Neural network output: [${biased[0,0]}, ${biased[1,0]}, ${biased[2,0]}]") +---- + +== What You'll Learn + +By following this guide, you'll learn to: + +1. **Set up your development environment** - Dependencies, imports, and basic project structure +2. **Master tensor creation and manipulation** - Different ways to create and work with tensors +3. **Perform matrix operations** - From basic arithmetic to advanced linear algebra +4. **Build neural network components** - Forward passes, activations, and simple networks +5. **Process real data** - Practical examples with image and numerical data +6. **Optimize performance** - Tips for efficient tensor operations + +== Prerequisites + +This guide assumes you have: + +* Basic knowledge of Kotlin programming +* Understanding of linear algebra concepts (vectors, matrices) +* Familiarity with machine learning concepts (helpful but not required) + +== Next Steps + +Ready to dive in? Start with xref:installation.adoc[Installation & Setup] to configure your development environment, then move on to xref:basic-tensors.adoc[Basic Tensor Operations] to learn the fundamentals. + +[TIP] +==== +Each section includes runnable code examples and explains both the "what" and the "why" behind each operation. Feel free to experiment with the examples and modify them to match your specific use case. +==== \ No newline at end of file diff --git a/docs/modules/getting-started/pages/installation.adoc b/docs/modules/getting-started/pages/installation.adoc new file mode 100644 index 000000000..3b4d7298f --- /dev/null +++ b/docs/modules/getting-started/pages/installation.adoc @@ -0,0 +1,187 @@ += Installation & Setup +:toc: left +:toclevels: 3 +:sectanchors: +:sectlinks: + +This section covers how to set up SKaiNET in your project and configure your development environment. + +== Adding SKaiNET to Your Project + +=== Kotlin Multiplatform Project + +Add SKaiNET to your multiplatform project's `build.gradle.kts`: + +[source,kotlin] +---- +kotlin { + sourceSets { + commonMain { + dependencies { + implementation("sk.ainet:tensors-api:1.0.0") + implementation("sk.ai.net:skainet-tensors:1.0.0") + } + } + } +} +---- + +=== JVM-only Project + +For JVM-only projects, add these dependencies to your `build.gradle.kts`: + +[source,kotlin] +---- +dependencies { + implementation("sk.ai.net:skainet-core:1.0.0") + implementation("sk.ai.net:skainet-tensors:1.0.0") +} +---- + +== Basic Project Structure + +Create a simple project structure to get started: + +[source,text] +---- +my-skainet-project/ +├── build.gradle.kts +├── settings.gradle.kts +└── src/ + └── commonMain/kotlin/ + └── Main.kt +---- + +== Your First SKaiNET Program + +Create `src/commonMain/kotlin/Main.kt` with this simple example: + +[source,kotlin] +---- +import sk.ai.net.core.tensor.* +import sk.ai.net.core.tensor.backend.* + +fun main() { + // Initialize the CPU backend + val backend = CpuBackend() + + // Create a simple 2x2 matrix + val matrix = CpuTensorFP32.fromArray( + Shape(2, 2), + floatArrayOf(1.0f, 2.0f, 3.0f, 4.0f) + ) + + // Print the matrix values + println("Matrix values:") + for (i in 0 until 2) { + for (j in 0 until 2) { + print("${matrix[i, j]} ") + } + println() + } + + // Perform a simple operation + val doubled = backend.scale(matrix, 2.0) + println("Doubled matrix:") + for (i in 0 until 2) { + for (j in 0 until 2) { + print("${doubled[i, j]} ") + } + println() + } +} +---- + +Run this program to verify your installation works correctly. You should see: + +[source,text] +---- +Matrix values: +1.0 2.0 +3.0 4.0 +Doubled matrix: +2.0 4.0 +6.0 8.0 +---- + +== Essential Imports + +For most SKaiNET operations, you'll need these imports: + +[source,kotlin] +---- +// Core tensor interfaces and data types +import sk.ai.net.core.tensor.* + +// Backend implementations +import sk.ai.net.core.tensor.backend.* +---- + +== Development Environment Tips + +=== IDE Setup + +* **IntelliJ IDEA**: Recommended IDE with excellent Kotlin multiplatform support +* **Android Studio**: Good for Android-specific tensor operations +* **VS Code**: Works with Kotlin plugin for lighter development + +=== Performance Considerations + +* **Debug vs Release**: Tensor operations are significantly faster in release builds +* **Memory Management**: Consider tensor lifecycle in long-running applications +* **Backend Selection**: Currently CPU backend is available, GPU backends coming soon + +=== Testing Your Setup + +Create a simple test to verify everything works: + +[source,kotlin] +---- +import sk.ai.net.core.tensor.* +import sk.ai.net.core.tensor.backend.* +import kotlin.test.* + +class SetupTest { + @Test + fun testBasicTensorOperations() { + val backend = CpuBackend() + + val a = CpuTensorFP32.fromArray(Shape(2, 2), floatArrayOf(1f, 2f, 3f, 4f)) + val b = CpuTensorFP32.fromArray(Shape(2, 2), floatArrayOf(5f, 6f, 7f, 8f)) + + val result = backend.matmul(a, b) + + // Verify the result: [1*5+2*7, 1*6+2*8] = [19, 22] + // [3*5+4*7, 3*6+4*8] = [43, 50] + assertEquals(19f, result[0, 0]) + assertEquals(22f, result[0, 1]) + assertEquals(43f, result[1, 0]) + assertEquals(50f, result[1, 1]) + } +} +---- + +== Common Setup Issues + +=== Dependency Resolution + +If you encounter dependency issues: + +1. Check that you're using compatible versions +2. Ensure your Gradle version supports Kotlin multiplatform +3. Verify network connectivity for dependency downloads + +=== Platform-Specific Issues + +* **JVM**: Requires Java 11 or higher +* **Android**: Minimum SDK 21 (Android 5.0) +* **Native**: Platform-specific toolchains required + +== Next Steps + +Now that you have SKaiNET set up, let's explore tensor operations in xref:basic-tensors.adoc[Basic Tensor Operations]. + +[TIP] +==== +Keep your first examples simple and gradually build complexity. The tensor operations are the foundation for everything else in SKaiNET. +==== \ No newline at end of file diff --git a/docs/modules/getting-started/pages/matrix-operations.adoc b/docs/modules/getting-started/pages/matrix-operations.adoc new file mode 100644 index 000000000..6059d2e5b --- /dev/null +++ b/docs/modules/getting-started/pages/matrix-operations.adoc @@ -0,0 +1,362 @@ += Matrix Operations +:toc: left +:toclevels: 3 +:sectanchors: +:sectlinks: + +Master matrix operations in SKaiNET with practical examples that form the foundation of machine learning and scientific computing. + +== Matrix Multiplication Fundamentals + +Matrix multiplication is the cornerstone of linear algebra and machine learning. SKaiNET provides efficient matrix multiplication through the `matmul` operation. + +=== Basic Matrix Multiplication + +[source,kotlin] +---- +val backend = CpuBackend() + +// Create two matrices +val A = CpuTensorFP32.fromArray( + Shape(2, 3), + floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) +) +// A = [[1, 2, 3], +// [4, 5, 6]] + +val B = CpuTensorFP32.fromArray( + Shape(3, 2), + floatArrayOf(7f, 8f, 9f, 10f, 11f, 12f) +) +// B = [[7, 8 ], +// [9, 10], +// [11, 12]] + +// Matrix multiplication: A × B +val C = backend.matmul(A, B) +// C = [[58, 64], // 1×7 + 2×9 + 3×11 = 58, 1×8 + 2×10 + 3×12 = 64 +// [139, 154]] // 4×7 + 5×9 + 6×11 = 139, 4×8 + 5×10 + 6×12 = 154 + +println("Result shape: ${C.shape}") // Shape(2, 2) +println("C[0,0] = ${C[0, 0]}, C[0,1] = ${C[0, 1]}") +println("C[1,0] = ${C[1, 0]}, C[1,1] = ${C[1, 1]}") +---- + +== Practical Example: Linear Transformations + +Transform 2D points using transformation matrices: + +[source,kotlin] +---- +// Rotation matrix for 45 degrees +fun createRotationMatrix(angleRadians: Float): CpuTensorFP32 { + val cos = kotlin.math.cos(angleRadians) + val sin = kotlin.math.sin(angleRadians) + return CpuTensorFP32.fromArray( + Shape(2, 2), + floatArrayOf(cos, -sin, sin, cos) + ) +} + +// Scaling matrix +fun createScalingMatrix(scaleX: Float, scaleY: Float): CpuTensorFP32 { + return CpuTensorFP32.fromArray( + Shape(2, 2), + floatArrayOf(scaleX, 0f, 0f, scaleY) + ) +} + +val backend = CpuBackend() + +// Original points: (1,0), (0,1), (-1,0), (0,-1) - unit circle +val points = CpuTensorFP32.fromArray( + Shape(2, 4), + floatArrayOf(1f, 0f, -1f, 0f, 0f, 1f, 0f, -1f) +) + +// Rotate by 45 degrees (π/4 radians) +val rotationMatrix = createRotationMatrix(kotlin.math.PI.toFloat() / 4) +val rotatedPoints = backend.matmul(rotationMatrix, points) + +// Scale by factor of 2 +val scalingMatrix = createScalingMatrix(2f, 2f) +val scaledAndRotated = backend.matmul(scalingMatrix, rotatedPoints) + +println("Original points:") +for (i in 0 until 4) { + println("(${points[0, i]}, ${points[1, i]})") +} + +println("After rotation and scaling:") +for (i in 0 until 4) { + println("(${scaledAndRotated[0, i]}, ${scaledAndRotated[1, i]})") +} +---- + +== Practical Example: System of Linear Equations + +Solve Ax = b using matrix operations (simplified Gaussian elimination approach): + +[source,kotlin] +---- +// Solve a 2x2 system: +// 3x + 2y = 7 +// 1x + 4y = 11 +fun solveLinearSystem2x2( + A: CpuTensorFP32, + b: CpuTensorFP32, + backend: CpuBackend +): CpuTensorFP32 { + // For 2x2 system, we can use Cramer's rule + // x = (b1*a22 - b2*a12) / det + // y = (a11*b2 - a21*b1) / det + + val a11 = A[0, 0]; val a12 = A[0, 1] + val a21 = A[1, 0]; val a22 = A[1, 1] + val b1 = b[0]; val b2 = b[1] + + val determinant = a11 * a22 - a12 * a21 + + val x = (b1 * a22 - b2 * a12) / determinant + val y = (a11 * b2 - a21 * b1) / determinant + + return CpuTensorFP32.fromArray(Shape(2), floatArrayOf(x, y)) +} + +val backend = CpuBackend() + +// Coefficient matrix A +val A = CpuTensorFP32.fromArray( + Shape(2, 2), + floatArrayOf(3f, 2f, 1f, 4f) +) + +// Constants vector b +val b = CpuTensorFP32.fromArray(Shape(2), floatArrayOf(7f, 11f)) + +val solution = solveLinearSystem2x2(A, b, backend) + +println("Solution: x = ${solution[0]}, y = ${solution[1]}") +// Should output: x = 1.0, y = 2.0 + +// Verify: A × solution should equal b +val verification = backend.matmul(A, solution.reshape(Shape(2, 1))) +println("Verification: Ax = [${verification[0, 0]}, ${verification[1, 0]}]") +---- + +== Vector Operations + +=== Dot Product + +Calculate the dot product of two vectors: + +[source,kotlin] +---- +val backend = CpuBackend() + +val vector1 = CpuTensorFP32.fromArray(Shape(3), floatArrayOf(1f, 2f, 3f)) +val vector2 = CpuTensorFP32.fromArray(Shape(3), floatArrayOf(4f, 5f, 6f)) + +// Dot product using the backend +val dotProduct = backend.dot(vector1, vector2) +println("Dot product: $dotProduct") // 1×4 + 2×5 + 3×6 = 32 + +// Alternative: using matrix multiplication for dot product +val dotProductAlt = backend.matmul( + vector1.reshape(Shape(1, 3)), + vector2.reshape(Shape(3, 1)) +)[0, 0] +println("Dot product (alternative): $dotProductAlt") +---- + +=== Vector-Matrix Operations + +[source,kotlin] +---- +val backend = CpuBackend() + +// Row vector × Matrix +val rowVector = CpuTensorFP32.fromArray(Shape(1, 3), floatArrayOf(1f, 2f, 3f)) +val matrix = CpuTensorFP32.fromArray( + Shape(3, 4), + floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f) +) + +val result1 = backend.matmul(rowVector, matrix) +println("Row vector × Matrix result shape: ${result1.shape}") // Shape(1, 4) + +// Matrix × Column vector +val columnVector = CpuTensorFP32.fromArray(Shape(4, 1), floatArrayOf(1f, 2f, 3f, 4f)) +val result2 = backend.matmul(matrix, columnVector) +println("Matrix × Column vector result shape: ${result2.shape}") // Shape(3, 1) +---- + +== Practical Example: Image Convolution (Simplified) + +Implement a basic image filter using matrix operations: + +[source,kotlin] +---- +// Simple 3x3 edge detection kernel +fun createEdgeDetectionKernel(): CpuTensorFP32 { + return CpuTensorFP32.fromArray( + Shape(3, 3), + floatArrayOf( + -1f, -1f, -1f, + -1f, 8f, -1f, + -1f, -1f, -1f + ) + ) +} + +// Apply kernel to a 3x3 image patch +fun applyKernel( + imagePatch: CpuTensorFP32, + kernel: CpuTensorFP32, + backend: CpuBackend +): Float { + // Element-wise multiplication and sum + val multiplied = with(backend) { imagePatch * kernel } + + var sum = 0f + for (i in 0 until 3) { + for (j in 0 until 3) { + sum += multiplied[i, j] + } + } + return sum +} + +val backend = CpuBackend() + +// Sample 3x3 image patch (grayscale values) +val imagePatch = CpuTensorFP32.fromArray( + Shape(3, 3), + floatArrayOf( + 100f, 100f, 100f, + 100f, 200f, 100f, // Bright center + 100f, 100f, 100f + ) +) + +val kernel = createEdgeDetectionKernel() +val filterResponse = applyKernel(imagePatch, kernel, backend) + +println("Edge detection response: $filterResponse") +// High value indicates an edge was detected +---- + +== Practical Example: Principal Component Analysis (PCA) Setup + +Prepare data for dimensionality reduction: + +[source,kotlin] +---- +// Center the data by subtracting the mean +fun centerData(data: CpuTensorFP32, backend: CpuBackend): CpuTensorFP32 { + val numSamples = data.shape.dimensions[0] + val numFeatures = data.shape.dimensions[1] + + // Calculate means for each feature + val means = FloatArray(numFeatures) { featureIndex -> + var sum = 0f + for (sampleIndex in 0 until numSamples) { + sum += data[sampleIndex, featureIndex] + } + sum / numSamples + } + + // Subtract means from each feature + val centered = FloatArray(numSamples * numFeatures) { i -> + val sampleIndex = i / numFeatures + val featureIndex = i % numFeatures + data[sampleIndex, featureIndex] - means[featureIndex] + } + + return CpuTensorFP32.fromArray(data.shape, centered) +} + +val backend = CpuBackend() + +// Sample dataset: 4 samples, 3 features +val rawData = CpuTensorFP32.fromArray( + Shape(4, 3), + floatArrayOf( + 1f, 2f, 3f, + 4f, 5f, 6f, + 7f, 8f, 9f, + 10f, 11f, 12f + ) +) + +val centeredData = centerData(rawData, backend) + +println("Original data means per feature:") +for (feature in 0 until 3) { + var sum = 0f + for (sample in 0 until 4) { + sum += rawData[sample, feature] + } + println("Feature $feature mean: ${sum / 4}") +} + +println("Centered data means per feature (should be ~0):") +for (feature in 0 until 3) { + var sum = 0f + for (sample in 0 until 4) { + sum += centeredData[sample, feature] + } + println("Feature $feature mean: ${sum / 4}") +} +---- + +== Performance Tips for Matrix Operations + +=== Choose Appropriate Shapes + +Matrix multiplication performance depends on shape: + +[source,kotlin] +---- +val backend = CpuBackend() + +// Efficient: Multiply smaller matrices when possible +val A = CpuTensorFP32.fromArray(Shape(100, 50), FloatArray(100 * 50) { it.toFloat() }) +val B = CpuTensorFP32.fromArray(Shape(50, 200), FloatArray(50 * 200) { it.toFloat() }) + +// This is more efficient than creating one large matrix +val result = backend.matmul(A, B) // Shape(100, 200) +---- + +=== Batch Operations + +Process multiple matrices together when possible: + +[source,kotlin] +---- +// Instead of multiple small operations, batch them +val backend = CpuBackend() + +// Batch matrix: multiple samples in one tensor +val batchMatrix = CpuTensorFP32.fromArray( + Shape(32, 128), // 32 samples, 128 features each + FloatArray(32 * 128) { kotlin.random.Random.nextFloat() } +) + +val weights = CpuTensorFP32.fromArray( + Shape(128, 64), // Transform to 64 features + FloatArray(128 * 64) { kotlin.random.Random.nextFloat() } +) + +// One operation processes all samples +val batchResult = backend.matmul(batchMatrix, weights) // Shape(32, 64) +---- + +== Next Steps + +With matrix operations mastered, you're ready for more advanced topics in xref:linear-algebra.adoc[Linear Algebra Examples] and xref:neural-network-basics.adoc[Neural Network Basics]. + +[TIP] +==== +Matrix operations are the building blocks of machine learning. Understanding how data flows through these transformations is key to designing effective neural networks and understanding algorithm behavior. +==== \ No newline at end of file diff --git a/docs/modules/getting-started/pages/neural-network-basics.adoc b/docs/modules/getting-started/pages/neural-network-basics.adoc new file mode 100644 index 000000000..d5e1ae8f5 --- /dev/null +++ b/docs/modules/getting-started/pages/neural-network-basics.adoc @@ -0,0 +1,422 @@ += Neural Network Basics +:toc: left +:toclevels: 3 +:sectanchors: +:sectlinks: + +Learn to build neural networks from scratch using SKaiNET's tensor operations. This section demonstrates the fundamental building blocks of deep learning. + +== Neural Network Components + +Neural networks consist of layers that transform input data through matrix operations and activation functions. + +=== The Linear Layer + +The most fundamental component - a fully connected layer: + +[source,kotlin] +---- +class LinearLayer( + val inputSize: Int, + val outputSize: Int, + val backend: CpuBackend +) { + // Weight matrix: outputSize × inputSize + val weights: CpuTensorFP32 + + // Bias vector: outputSize × 1 + val bias: CpuTensorFP32 + + init { + // Initialize weights with small random values + val weightData = FloatArray(outputSize * inputSize) { + (kotlin.random.Random.nextGaussian().toFloat() * 0.1f) + } + weights = CpuTensorFP32.fromArray(Shape(outputSize, inputSize), weightData) + + // Initialize bias to zeros + val biasData = FloatArray(outputSize) { 0f } + bias = CpuTensorFP32.fromArray(Shape(outputSize, 1), biasData) + } + + // Forward pass: output = weights × input + bias + fun forward(input: CpuTensorFP32): CpuTensorFP32 { + val inputReshaped = input.reshape(Shape(inputSize, 1)) + val weightedSum = backend.matmul(weights, inputReshaped) + + return with(backend) { + weightedSum + bias + } + } +} + +// Example usage +val backend = CpuBackend() + +// Create a layer that transforms 3 inputs to 2 outputs +val layer = LinearLayer(3, 2, backend) + +// Sample input +val input = CpuTensorFP32.fromArray(Shape(3), floatArrayOf(1f, 0.5f, -0.2f)) +val output = layer.forward(input) + +println("Input shape: ${input.shape}") +println("Output shape: ${output.shape}") +println("Output: [${output[0, 0]}, ${output[1, 0]}]") +---- + +== Activation Functions + +Activation functions introduce non-linearity, enabling networks to learn complex patterns. + +=== ReLU Activation + +The most commonly used activation function: + +[source,kotlin] +---- +fun relu(tensor: CpuTensorFP32, backend: CpuBackend): CpuTensorFP32 { + val shape = tensor.shape + val result = FloatArray(shape.size) { i -> + val indices = indexToCoordinates(i, shape) + val value = tensor[*indices] + if (value > 0f) value else 0f + } + return CpuTensorFP32.fromArray(shape, result) +} + +// Helper function to convert flat index to coordinates +fun indexToCoordinates(index: Int, shape: Shape): IntArray { + val coords = IntArray(shape.rank) + var remaining = index + for (i in shape.rank - 1 downTo 0) { + val dimSize = shape.dimensions[i] + coords[i] = remaining % dimSize + remaining /= dimSize + } + return coords +} + +// Example usage +val backend = CpuBackend() +val input = CpuTensorFP32.fromArray( + Shape(3), + floatArrayOf(-1f, 0f, 2f) +) + +val activated = relu(input, backend) +println("Input: [-1, 0, 2]") +println("ReLU output: [${activated[0]}, ${activated[1]}, ${activated[2]}]") +// Output: [0, 0, 2] +---- + +=== Sigmoid Activation + +Useful for binary classification: + +[source,kotlin] +---- +fun sigmoid(tensor: CpuTensorFP32, backend: CpuBackend): CpuTensorFP32 { + val shape = tensor.shape + val result = FloatArray(shape.size) { i -> + val indices = indexToCoordinates(i, shape) + val value = tensor[*indices] + 1f / (1f + kotlin.math.exp(-value)) + } + return CpuTensorFP32.fromArray(shape, result) +} + +// Example usage +val input = CpuTensorFP32.fromArray( + Shape(3), + floatArrayOf(-2f, 0f, 2f) +) + +val activated = sigmoid(input, backend) +println("Input: [-2, 0, 2]") +println("Sigmoid output: [${activated[0]}, ${activated[1]}, ${activated[2]}]") +// Output: [~0.12, 0.5, ~0.88] +---- + +== Building a Simple Neural Network + +Let's create a 2-layer neural network for binary classification: + +[source,kotlin] +---- +class SimpleNeuralNetwork( + val inputSize: Int, + val hiddenSize: Int, + val outputSize: Int, + val backend: CpuBackend +) { + private val layer1 = LinearLayer(inputSize, hiddenSize, backend) + private val layer2 = LinearLayer(hiddenSize, outputSize, backend) + + fun forward(input: CpuTensorFP32): CpuTensorFP32 { + // First layer + ReLU activation + val hidden = layer1.forward(input) + val hiddenActivated = relu(hidden, backend) + + // Second layer + Sigmoid activation + val output = layer2.forward(hiddenActivated.reshape(Shape(hiddenSize))) + return sigmoid(output, backend) + } + + // Predict class (0 or 1) based on threshold + fun predict(input: CpuTensorFP32): Int { + val output = forward(input) + return if (output[0, 0] > 0.5f) 1 else 0 + } +} + +// Example usage +val backend = CpuBackend() + +// Create a network: 2 inputs -> 4 hidden -> 1 output +val network = SimpleNeuralNetwork(2, 4, 1, backend) + +// Test with sample data points +val testData = arrayOf( + floatArrayOf(0f, 0f), // Class 0 + floatArrayOf(0f, 1f), // Class 1 + floatArrayOf(1f, 0f), // Class 1 + floatArrayOf(1f, 1f) // Class 0 (XOR pattern) +) + +println("Neural Network Predictions (XOR-like pattern):") +testData.forEachIndexed { index, data -> + val input = CpuTensorFP32.fromArray(Shape(2), data) + val output = network.forward(input) + val prediction = network.predict(input) + + println("Input: [${data[0]}, ${data[1]}] -> Output: ${output[0, 0]} -> Prediction: $prediction") +} +---- + +== Practical Example: Linear Regression + +Implement simple linear regression using neural networks: + +[source,kotlin] +---- +// Generate synthetic data: y = 2x + 1 + noise +fun generateLinearData(numSamples: Int): Pair { + val inputs = FloatArray(numSamples) { kotlin.random.Random.nextFloat() * 10f } + val outputs = FloatArray(numSamples) { i -> + 2f * inputs[i] + 1f + (kotlin.random.Random.nextGaussian().toFloat() * 0.1f) + } + + return Pair( + CpuTensorFP32.fromArray(Shape(numSamples, 1), inputs), + CpuTensorFP32.fromArray(Shape(numSamples, 1), outputs) + ) +} + +// Simple linear regression model +class LinearRegression(val backend: CpuBackend) { + // Single linear layer without bias for simplicity + var weight = CpuTensorFP32.fromArray(Shape(1, 1), floatArrayOf(1f)) + var bias = CpuTensorFP32.fromArray(Shape(1, 1), floatArrayOf(0f)) + + fun forward(input: CpuTensorFP32): CpuTensorFP32 { + val prediction = backend.matmul(input, weight) + return with(backend) { prediction + bias } + } + + // Simple gradient descent step (simplified) + fun trainStep(inputs: CpuTensorFP32, targets: CpuTensorFP32, learningRate: Float) { + val predictions = forward(inputs) + val numSamples = inputs.shape.dimensions[0].toFloat() + + // Calculate errors + val errors = with(backend) { predictions - targets } + + // Calculate gradients (simplified) + var weightGradient = 0f + var biasGradient = 0f + + for (i in 0 until inputs.shape.dimensions[0]) { + val input = inputs[i, 0] + val error = errors[i, 0] + weightGradient += error * input + biasGradient += error + } + + weightGradient /= numSamples + biasGradient /= numSamples + + // Update parameters + val newWeight = weight[0, 0] - learningRate * weightGradient + val newBias = bias[0, 0] - learningRate * biasGradient + + weight = CpuTensorFP32.fromArray(Shape(1, 1), floatArrayOf(newWeight)) + bias = CpuTensorFP32.fromArray(Shape(1, 1), floatArrayOf(newBias)) + } +} + +// Training example +val backend = CpuBackend() +val model = LinearRegression(backend) + +// Generate training data +val (trainInputs, trainTargets) = generateLinearData(100) + +println("Training Linear Regression...") +println("Initial: weight=${model.weight[0, 0]}, bias=${model.bias[0, 0]}") + +// Simple training loop +repeat(1000) { epoch -> + model.trainStep(trainInputs, trainTargets, 0.001f) + + if (epoch % 200 == 0) { + println("Epoch $epoch: weight=${model.weight[0, 0]}, bias=${model.bias[0, 0]}") + } +} + +println("Final: weight=${model.weight[0, 0]}, bias=${model.bias[0, 0]}") +println("Expected: weight=2.0, bias=1.0") +---- + +== Practical Example: Image Classification + +Build a simple neural network for MNIST-like digit classification: + +[source,kotlin] +---- +// Simplified 28x28 image classifier +class SimpleImageClassifier(val backend: CpuBackend) { + // Flatten 28x28 image to 784 features + private val layer1 = LinearLayer(784, 128, backend) + private val layer2 = LinearLayer(128, 64, backend) + private val layer3 = LinearLayer(64, 10, backend) // 10 digit classes + + fun forward(image: CpuTensorFP32): CpuTensorFP32 { + // Flatten image + val flattened = image.reshape(Shape(784)) + + // First layer + ReLU + val hidden1 = layer1.forward(flattened) + val activated1 = relu(hidden1, backend) + + // Second layer + ReLU + val hidden2 = layer2.forward(activated1.reshape(Shape(128))) + val activated2 = relu(hidden2, backend) + + // Output layer (logits) + return layer3.forward(activated2.reshape(Shape(64))) + } + + // Simple softmax for probability distribution + fun softmax(logits: CpuTensorFP32): CpuTensorFP32 { + val maxLogit = (0 until 10).map { logits[it, 0] }.maxOrNull() ?: 0f + + val expLogits = FloatArray(10) { i -> + kotlin.math.exp(logits[i, 0] - maxLogit) + } + + val sumExp = expLogits.sum() + val probabilities = expLogits.map { it / sumExp }.toFloatArray() + + return CpuTensorFP32.fromArray(Shape(10, 1), probabilities) + } + + fun predict(image: CpuTensorFP32): Int { + val logits = forward(image) + val probabilities = softmax(logits) + + return (0 until 10).maxByOrNull { probabilities[it, 0] } ?: 0 + } +} + +// Create a synthetic 28x28 "image" (random pattern) +fun createSyntheticImage(): CpuTensorFP32 { + val imageData = FloatArray(28 * 28) { kotlin.random.Random.nextFloat() } + return CpuTensorFP32.fromArray(Shape(28, 28), imageData) +} + +// Example usage +val backend = CpuBackend() +val classifier = SimpleImageClassifier(backend) + +val testImage = createSyntheticImage() +val prediction = classifier.predict(testImage) + +println("Image shape: ${testImage.shape}") +println("Predicted digit: $prediction") + +// Show probability distribution +val logits = classifier.forward(testImage) +val probabilities = classifier.softmax(logits) + +println("Class probabilities:") +for (digit in 0 until 10) { + println("Digit $digit: ${probabilities[digit, 0]}") +} +---- + +== Performance and Memory Tips + +=== Batch Processing + +Process multiple samples together for better efficiency: + +[source,kotlin] +---- +// Instead of processing one sample at a time +fun processIndividually(samples: List, network: SimpleNeuralNetwork): List { + return samples.map { network.forward(it) } +} + +// Process in batches +fun processBatch(samples: List, network: SimpleNeuralNetwork, backend: CpuBackend): CpuTensorFP32 { + // Stack samples into a batch tensor + val batchSize = samples.size + val inputSize = samples[0].shape.dimensions[0] + + val batchData = FloatArray(batchSize * inputSize) { i -> + val sampleIndex = i / inputSize + val featureIndex = i % inputSize + samples[sampleIndex][featureIndex] + } + + val batchTensor = CpuTensorFP32.fromArray(Shape(batchSize, inputSize), batchData) + + // Process entire batch at once (requires batch-aware network implementation) + // This is more efficient than individual processing + return batchTensor +} +---- + +=== Memory Management + +[source,kotlin] +---- +// Reuse tensors when possible +class EfficientLinearLayer(inputSize: Int, outputSize: Int, val backend: CpuBackend) { + val weights = CpuTensorFP32.fromArray( + Shape(outputSize, inputSize), + FloatArray(outputSize * inputSize) { kotlin.random.Random.nextGaussian().toFloat() * 0.1f } + ) + + // Pre-allocate output tensor to avoid repeated allocations + private val outputBuffer = CpuTensorFP32.fromArray( + Shape(outputSize, 1), + FloatArray(outputSize) { 0f } + ) + + fun forward(input: CpuTensorFP32): CpuTensorFP32 { + // Reuse buffer instead of creating new tensors + return backend.matmul(weights, input.reshape(Shape(input.shape.dimensions[0], 1))) + } +} +---- + +== Next Steps + +Now that you understand neural network basics, explore more advanced topics in xref:data-processing.adoc[Data Processing Use Cases] to see how these networks handle real-world data. + +[TIP] +==== +Neural networks are compositions of simple mathematical operations. Understanding how gradients flow backward through these operations is crucial for training - this foundation will help you understand more advanced architectures and training techniques. +==== \ No newline at end of file diff --git a/docs/modules/getting-started/pages/performance-tips.adoc b/docs/modules/getting-started/pages/performance-tips.adoc new file mode 100644 index 000000000..a0352d649 --- /dev/null +++ b/docs/modules/getting-started/pages/performance-tips.adoc @@ -0,0 +1,129 @@ += Performance Tips +:toc: left +:toclevels: 3 +:sectanchors: +:sectlinks: + +Optimize your SKaiNET applications for better performance and efficient memory usage. + +== General Performance Guidelines + +=== Choose the Right Data Types + +Use appropriate tensor types for your use case: + +[source,kotlin] +---- +// For most ML applications, FP32 provides good balance +val tensor = CpuTensorFP32.fromArray(shape, data) + +// Consider precision requirements vs. performance trade-offs +---- + +=== Minimize Memory Allocations + +Reuse tensors when possible to reduce garbage collection pressure: + +[source,kotlin] +---- +class EfficientProcessor(val backend: CpuBackend) { + // Pre-allocate buffers + private val workBuffer = CpuTensorFP32.fromArray( + Shape(1024, 1024), + FloatArray(1024 * 1024) + ) + + fun process(input: CpuTensorFP32): CpuTensorFP32 { + // Reuse pre-allocated buffer instead of creating new tensors + return backend.matmul(input, workBuffer) + } +} +---- + +== Matrix Operation Optimization + +=== Batch Operations + +Process multiple samples together: + +[source,kotlin] +---- +// Efficient: Process batch of data +val batchInput = CpuTensorFP32.fromArray(Shape(32, 784), batchData) +val weights = CpuTensorFP32.fromArray(Shape(784, 128), weightData) +val batchOutput = backend.matmul(batchInput, weights) + +// Less efficient: Process one by one +val outputs = inputs.map { input -> + backend.matmul(input, weights) +} +---- + +=== Choose Appropriate Matrix Sizes + +Consider the computational complexity: + +[source,kotlin] +---- +// Matrix multiplication is O(n³), so size matters +// Prefer smaller intermediate representations when possible + +// Instead of: input(1000) -> hidden(2000) -> output(10) +// Consider: input(1000) -> hidden(500) -> hidden(128) -> output(10) +---- + +== Memory Management + +=== Monitor Memory Usage + +[source,kotlin] +---- +fun processLargeDataset(data: List) { + data.chunked(100).forEach { chunk -> + // Process chunk + chunk.forEach { tensor -> + // Process tensor + } + + // Optional: Suggest garbage collection + System.gc() + } +} +---- + +=== Avoid Memory Leaks + +[source,kotlin] +---- +// Don't hold references to large tensors unnecessarily +class DataProcessor { + // Bad: Keeping reference to large tensor + // private var lastResult: CpuTensorFP32? = null + + // Good: Process and release + fun process(input: CpuTensorFP32): Float { + val result = backend.matmul(input, weights) + val summary = result[0, 0] // Extract what you need + // result goes out of scope and can be garbage collected + return summary + } +} +---- + +== Best Practices Summary + +1. **Use batch processing** for multiple samples +2. **Pre-allocate buffers** for repeated operations +3. **Choose appropriate tensor sizes** to balance memory and computation +4. **Monitor memory usage** in long-running applications +5. **Release references** to large tensors when done +6. **Profile your application** to identify bottlenecks + +== Next Steps + +You now have a solid foundation in SKaiNET! Explore the xref:../arc42/pages/01-introduction-and-goals.adoc[Architecture Documentation] to understand the framework's design principles. + +[TIP] +==== +Performance optimization should be done after your application is working correctly. Profile first, then optimize the bottlenecks you actually encounter. +==== \ No newline at end of file From 7bd2ce360deabcf5a895c95c25c1102a14847048 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 10:54:45 +0200 Subject: [PATCH 08/16] Rename usage of the name skainet to SKaiNET Related-To: #101 --- GITFLOW.adoc | 4 ++-- README.adoc | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GITFLOW.adoc b/GITFLOW.adoc index 46a3b6824..1dd8eaf10 100644 --- a/GITFLOW.adoc +++ b/GITFLOW.adoc @@ -5,7 +5,7 @@ :sectlinks: :source-highlighter: highlight.js -This document describes the GitFlow workflow used in the skainet project for managing feature development, releases, and hotfixes. +This document describes the GitFlow workflow used in the SKaiNET project for managing feature development, releases, and hotfixes. == Overview @@ -309,6 +309,6 @@ git reset --hard HEAD~1 == Conclusion -GitFlow provides a structured approach to managing code changes in collaborative environments. By following this workflow consistently, the skainet project maintains code quality, enables parallel development, and ensures stable releases. +GitFlow provides a structured approach to managing code changes in collaborative environments. By following this workflow consistently, the SKaiNET project maintains code quality, enables parallel development, and ensures stable releases. For questions or clarifications about this workflow, please refer to the original https://nvie.com/posts/a-successful-git-branching-model/[GitFlow article] or reach out to the project maintainers. \ No newline at end of file diff --git a/README.adoc b/README.adoc index e1b7d31b0..721b71f9e 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,6 @@ -= skainet += SKaiNET -*skainet* is an open-source deep learning framework written in Kotlin, designed with developers in mind to enable the creation modern AI powered applications with ease. +*SKaiNET* is an open-source deep learning framework written in Kotlin, designed with developers in mind to enable the creation modern AI powered applications with ease. == Development Practices From 73fd383f9f87eda2022fe506b0ba2dd7b0fc292a Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 12:54:42 +0200 Subject: [PATCH 09/16] Add initial version of performance measurement Related-To: #36 --- docs/modules/performance/nav.adoc | 4 + docs/modules/performance/pages/index.adoc | 70 +++++++ .../skainet-performance/build.gradle.kts | 59 ++++++ .../skainet-performance/gradle.properties | 3 + .../ainet/core/performance/BenchmarkResult.kt | 182 ++++++++++++++++++ .../ainet/core/performance/BenchmarkRunner.kt | 93 +++++++++ 6 files changed, 411 insertions(+) create mode 100644 docs/modules/performance/nav.adoc create mode 100644 docs/modules/performance/pages/index.adoc create mode 100644 skainet-core/skainet-performance/build.gradle.kts create mode 100644 skainet-core/skainet-performance/gradle.properties create mode 100644 skainet-core/skainet-performance/src/commonMain/kotlin/sk/ainet/core/performance/BenchmarkResult.kt create mode 100644 skainet-core/skainet-performance/src/commonMain/kotlin/sk/ainet/core/performance/BenchmarkRunner.kt diff --git a/docs/modules/performance/nav.adoc b/docs/modules/performance/nav.adoc new file mode 100644 index 000000000..cb920e4f7 --- /dev/null +++ b/docs/modules/performance/nav.adoc @@ -0,0 +1,4 @@ +* xref:index.adoc[Performance Measurement] +** xref:benchmark-framework.adoc[Benchmark Framework] +** xref:performance-testing.adoc[Performance Testing Guide] +** xref:metrics-analysis.adoc[Metrics and Analysis] \ No newline at end of file diff --git a/docs/modules/performance/pages/index.adoc b/docs/modules/performance/pages/index.adoc new file mode 100644 index 000000000..529a1da2b --- /dev/null +++ b/docs/modules/performance/pages/index.adoc @@ -0,0 +1,70 @@ += Performance Measurement Framework +:toc: +:toclevels: 3 +:sectanchors: +:sectlinks: + +The SKaiNET Performance Measurement Framework provides comprehensive tools for benchmarking tensor operations and analyzing computational performance across different backends and configurations. + +== Overview + +The performance measurement framework is designed to: + +* Measure execution time and throughput of tensor operations +* Compare performance between different compute backends +* Provide statistical analysis of benchmark results +* Generate detailed performance reports +* Support multiplatform benchmarking + +== Key Components + +=== BenchmarkRunner + +The `BenchmarkRunner` class is the core component that executes performance measurements: + +[source,kotlin] +---- +val runner = BenchmarkRunner() +val result = runner.benchmark( + name = "Matrix Multiplication 256x256", + warmupRuns = 10, + measurementRuns = 100 +) { + // Your operation to benchmark + backend.matmul(matrixA, matrixB) +} +---- + +=== Benchmark Results + +The framework provides comprehensive result analysis: + +* **TimeStatistics**: Mean, median, standard deviation, min/max execution times +* **MemoryStatistics**: Memory usage tracking (platform-dependent) +* **BenchmarkResult**: Complete benchmark results with metadata +* **BenchmarkReport**: Comprehensive reports for multiple operations + +=== Performance Analysis + +Advanced analysis capabilities include: + +* Statistical analysis of execution times +* Throughput calculations (operations per second) +* Performance comparison between backends +* Speedup analysis and regression detection + +== Quick Start + +. Add the performance module dependency to your test configuration +. Create a `BenchmarkRunner` instance +. Use the `benchmark()` method to measure your operations +. Analyze results using the provided data classes + +== Module Structure + +The performance module (`skainet-core:skainet-performance`) contains: + +* Core benchmarking infrastructure +* Result analysis and reporting tools +* Statistical utilities +* Multiplatform compatibility layer \ No newline at end of file diff --git a/skainet-core/skainet-performance/build.gradle.kts b/skainet-core/skainet-performance/build.gradle.kts new file mode 100644 index 000000000..3c3d7f5ab --- /dev/null +++ b/skainet-core/skainet-performance/build.gradle.kts @@ -0,0 +1,59 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.vanniktech.mavenPublish) +} + +kotlin { + explicitApi() + + androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + iosArm64() + iosSimulatorArm64() + macosArm64 () + linuxX64 () + linuxArm64 () + + jvm() + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":skainet-core:skainet-tensors-api")) + } + } + + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } +} + +android { + namespace = "sk.ai.net.core.performance" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} \ No newline at end of file diff --git a/skainet-core/skainet-performance/gradle.properties b/skainet-core/skainet-performance/gradle.properties new file mode 100644 index 000000000..b4e7606df --- /dev/null +++ b/skainet-core/skainet-performance/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=skainet-performance +POM_NAME=SKaiNET Performance Measurement Framework +POM_DESCRIPTION=Core performance measurement and benchmarking utilities for SKaiNET tensor operations \ No newline at end of file diff --git a/skainet-core/skainet-performance/src/commonMain/kotlin/sk/ainet/core/performance/BenchmarkResult.kt b/skainet-core/skainet-performance/src/commonMain/kotlin/sk/ainet/core/performance/BenchmarkResult.kt new file mode 100644 index 000000000..68ab98f13 --- /dev/null +++ b/skainet-core/skainet-performance/src/commonMain/kotlin/sk/ainet/core/performance/BenchmarkResult.kt @@ -0,0 +1,182 @@ +package sk.ainet.core.performance + +/** + * Comprehensive benchmark result containing timing statistics + */ +public data class BenchmarkResult( + val name: String, + val executionTime: TimeStatistics, + val memoryUsage: MemoryStatistics, + val throughput: Double, // operations per second + val metadata: Map +) { + public fun prettyPrint(): String { + return buildString { + appendLine("$name:") + appendLine(" Time: ${(executionTime.mean / 1000).format(3)}ms ± ${(executionTime.standardDeviation / 1000).format(3)}ms") + appendLine(" Throughput: ${throughput.format(1)} ops/sec") + appendLine(" Memory: ${formatBytes(memoryUsage.allocatedBytes)}") + appendLine(" Range: ${(executionTime.min / 1000).format(3)}ms - ${(executionTime.max / 1000).format(3)}ms") + } + } + + private fun formatBytes(bytes: Long): String { + return when { + bytes >= 1_000_000 -> "${(bytes / 1_000_000.0).format(1)}MB" + bytes >= 1_000 -> "${(bytes / 1_000.0).format(1)}KB" + else -> "${bytes}B" + } + } + + private fun Double.format(decimals: Int): String { + val multiplier = when (decimals) { + 0 -> 1.0 + 1 -> 10.0 + 2 -> 100.0 + 3 -> 1000.0 + else -> 1000.0 + } + val rounded = (this * multiplier).toInt() / multiplier + return rounded.toString() + } +} + +/** + * Statistical analysis of execution times + */ +public data class TimeStatistics( + val mean: Double, // microseconds + val median: Double, // microseconds + val standardDeviation: Double, // microseconds + val min: Double, // microseconds + val max: Double, // microseconds + val percentile95: Double // microseconds +) + +/** + * Memory usage statistics (placeholder for multiplatform compatibility) + */ +public data class MemoryStatistics( + val allocatedBytes: Long, + val peakHeapUsage: Long, + val gcCollections: Int, + val gcTime: Long // milliseconds +) + +/** + * Comprehensive benchmark report containing all operation results + */ +public data class BenchmarkReport( + val backendName: String, + val matrixMultiplication: List, + val elementwiseOperations: Map>, + val dotProduct: List, + val scalarOperations: Map>, + val summary: BenchmarkSummary +) { + public fun prettyPrint(): String { + return buildString { + appendLine("=" .repeat(50)) + appendLine("TENSOR BENCHMARK REPORT - $backendName") + appendLine("=" .repeat(50)) + appendLine() + + appendLine("MATRIX MULTIPLICATION:") + matrixMultiplication.forEach { result -> + appendLine(result.prettyPrint()) + } + appendLine() + + appendLine("ELEMENT-WISE OPERATIONS:") + elementwiseOperations.forEach { (opType, results) -> + appendLine(" $opType:") + results.forEach { result -> + appendLine(" ${result.prettyPrint().prependIndent(" ")}") + } + } + appendLine() + + appendLine("SUMMARY:") + appendLine(" Total Operations: ${summary.totalOperations}") + appendLine(" Average Throughput: ${summary.averageThroughput.format(1)} ops/sec") + appendLine(" Fastest: ${summary.fastestOperation}") + appendLine(" Slowest: ${summary.slowestOperation}") + } + } + + private fun Double.format(decimals: Int): String { + val multiplier = when (decimals) { + 0 -> 1.0 + 1 -> 10.0 + 2 -> 100.0 + 3 -> 1000.0 + else -> 1000.0 + } + val rounded = (this * multiplier).toInt() / multiplier + return rounded.toString() + } +} + +/** + * Summary statistics for a benchmark run + */ +public data class BenchmarkSummary( + val totalOperations: Int, + val averageThroughput: Double, + val fastestOperation: String, + val slowestOperation: String +) + +/** + * Backend comparison report showing speedup analysis + */ +public data class ComparisonReport( + val baselineBackend: String, + val baselineResults: BenchmarkReport, + val comparisonBackend: String, + val comparisonResults: BenchmarkReport, + val speedupAnalysis: SpeedupAnalysis +) { + public fun prettyPrint(): String { + return buildString { + appendLine("=" .repeat(60)) + appendLine("BACKEND COMPARISON: $baselineBackend vs $comparisonBackend") + appendLine("=" .repeat(60)) + appendLine() + + appendLine("SPEEDUP ANALYSIS:") + appendLine(" Average Speedup: ${speedupAnalysis.averageSpeedup.format(2)}x") + appendLine(" Best Case: ${speedupAnalysis.bestCaseSpeedup.format(2)}x") + appendLine(" Worst Case: ${speedupAnalysis.worstCaseSpeedup.format(2)}x") + appendLine() + + appendLine("DETAILED SPEEDUPS:") + speedupAnalysis.operationSpeedups.forEach { (operation, speedup) -> + appendLine(" $operation: ${speedup.format(2)}x") + } + } + } + + private fun Double.format(decimals: Int): String { + val multiplier = when (decimals) { + 0 -> 1.0 + 1 -> 10.0 + 2 -> 100.0 + 3 -> 1000.0 + else -> 1000.0 + } + val rounded = (this * multiplier).toInt() / multiplier + return rounded.toString() + } +} + +/** + * Speedup analysis comparing two backends + */ +public data class SpeedupAnalysis( + val operationSpeedups: Map, + val averageSpeedup: Double, + val bestCaseSpeedup: Double, + val worstCaseSpeedup: Double, + val speedupByTensorSize: Map +) \ No newline at end of file diff --git a/skainet-core/skainet-performance/src/commonMain/kotlin/sk/ainet/core/performance/BenchmarkRunner.kt b/skainet-core/skainet-performance/src/commonMain/kotlin/sk/ainet/core/performance/BenchmarkRunner.kt new file mode 100644 index 000000000..ba9d450ed --- /dev/null +++ b/skainet-core/skainet-performance/src/commonMain/kotlin/sk/ainet/core/performance/BenchmarkRunner.kt @@ -0,0 +1,93 @@ +package sk.ainet.core.performance + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.measureTime +import kotlin.time.TimeSource + +/** + * Core benchmark runner for measuring tensor operation performance. + * Multiplatform-compatible implementation using Kotlin stdlib APIs. + */ +public class BenchmarkRunner { + + /** + * Runs a comprehensive benchmark of the given operation. + * + * @param name Human-readable name for the benchmark + * @param warmupRuns Number of warmup iterations (default: 10) + * @param measurementRuns Number of measurement iterations (default: 100) + * @param operation The operation to benchmark + * @return Detailed benchmark results including timing statistics + */ + public fun benchmark( + name: String, + warmupRuns: Int = 10, + measurementRuns: Int = 100, + operation: () -> T + ): BenchmarkResult { + // Warmup phase to allow JIT compilation + repeat(warmupRuns) { + operation() + } + + val times = mutableListOf() + + // Measurement phase + repeat(measurementRuns) { + val time = measureTime { operation() }.inWholeMicroseconds.toDouble() + times.add(time) + } + + return BenchmarkResult( + name = name, + executionTime = calculateTimeStatistics(times), + memoryUsage = MemoryStatistics(0L, 0L, 0, 0L), // Placeholder for memory stats + throughput = calculateThroughput(times), + metadata = mapOf( + "warmupRuns" to warmupRuns, + "measurementRuns" to measurementRuns, + "timestamp" to TimeSource.Monotonic.markNow().elapsedNow().inWholeMilliseconds + ) + ) + } + + /** + * Advanced warmup with time-based minimum duration + */ + public fun warmupWithDuration( + operation: () -> Unit, + minRuns: Int = 10, + minTime: Duration = 5.seconds + ) { + val timeSource = TimeSource.Monotonic + val startMark = timeSource.markNow() + var runs = 0 + + while (runs < minRuns || startMark.elapsedNow() < minTime) { + operation() + runs++ + } + } + + private fun calculateTimeStatistics(times: List): TimeStatistics { + val sorted = times.sorted() + val mean = times.average() + val variance = times.map { (it - mean) * (it - mean) }.average() + val stdDev = kotlin.math.sqrt(variance) + + return TimeStatistics( + mean = mean, + median = sorted[sorted.size / 2], + standardDeviation = stdDev, + min = sorted.first(), + max = sorted.last(), + percentile95 = sorted[(sorted.size * 0.95).toInt()] + ) + } + + private fun calculateThroughput(times: List): Double { + val averageTimeMicros = times.average() + return 1_000_000.0 / averageTimeMicros // operations per second + } +} \ No newline at end of file From 5c9005765244ea68c76bdf004cb5a8b92cce057f Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 13:09:14 +0200 Subject: [PATCH 10/16] Add performance measurement also as module Related-To: #36 --- settings.gradle.kts | 1 + skainet-core/skainet-performance/build.gradle.kts | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 3c9efde5a..f2864d18e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,3 +17,4 @@ rootProject.name = "skainet" include("skainet-core:skainet-tensors-api") include("skainet-core:skainet-tensors") +include("skainet-core:skainet-performance") diff --git a/skainet-core/skainet-performance/build.gradle.kts b/skainet-core/skainet-performance/build.gradle.kts index 3c3d7f5ab..7893c7456 100644 --- a/skainet-core/skainet-performance/build.gradle.kts +++ b/skainet-core/skainet-performance/build.gradle.kts @@ -20,9 +20,9 @@ kotlin { iosArm64() iosSimulatorArm64() - macosArm64 () - linuxX64 () - linuxArm64 () + macosArm64() + linuxX64() + linuxArm64() jvm() @@ -33,10 +33,8 @@ kotlin { } sourceSets { - val commonMain by getting { - dependencies { - implementation(project(":skainet-core:skainet-tensors-api")) - } + commonMain.dependencies { + implementation(project(":skainet-core:skainet-tensors-api")) } commonTest.dependencies { From 074709b5fbc4020cab729d56655f9749e5f833de Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 13:16:31 +0200 Subject: [PATCH 11/16] Fix package names to be sk.ainet everywhere --- .../ainet/core/tensor/backend/CpuBackend.kt | 50 ++- .../sk/ainet/core/tensor/PrintingTest.kt | 8 +- .../kotlin/sk/ainet/core/tensor/TestUtils.kt | 2 +- .../core/tensor/backend/CpuBackendTest.kt | 4 +- .../benchmark/BaselinePerformanceTest.kt | 224 ++--------- .../core/tensor/benchmark/BenchmarkRunner.kt | 156 -------- .../tensor/benchmark/TensorBenchmarkSuite.kt | 347 ------------------ .../tensor/multiplication/EdgeCasesTest.kt | 5 +- .../ElementwiseMultiplicationTest.kt | 5 +- .../tensor/multiplication/MLOperationsTest.kt | 21 +- .../MatrixMultiplicationTest.kt | 96 ++--- 11 files changed, 144 insertions(+), 774 deletions(-) delete mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BenchmarkRunner.kt delete mode 100644 skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/TensorBenchmarkSuite.kt diff --git a/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt b/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt index 7664cc156..18b242815 100644 --- a/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt +++ b/skainet-core/skainet-tensors/src/commonMain/kotlin/sk/ainet/core/tensor/backend/CpuBackend.kt @@ -1,6 +1,7 @@ -package sk.ai.net.core.tensor.backend +package sk.ainet.core.tensor.backend import sk.ai.net.core.tensor.* +import sk.ai.net.core.tensor.backend.ComputeBackend /** * Convenient type alias for FP32 tensors with Float values. @@ -36,7 +37,7 @@ public fun TensorFP32.printMatrix(): String { val rows = shape[0] val cols = shape[1] val result = StringBuilder() - + result.append("[\n") for (i in 0 until rows) { result.append(" [") @@ -49,7 +50,7 @@ public fun TensorFP32.printMatrix(): String { result.append("\n") } result.append("]") - + return result.toString() } @@ -100,25 +101,46 @@ public class CpuTensorFP32( override fun dot(a: Tensor, b: Tensor): Double = backend.dot(a, b) // Tensor-Tensor operations - override fun Tensor.plus(other: Tensor): Tensor = with(backend) { this@plus.plus(other) } - override fun Tensor.minus(other: Tensor): Tensor = with(backend) { this@minus.minus(other) } - override fun Tensor.times(other: Tensor): Tensor = with(backend) { this@times.times(other) } - override fun Tensor.div(other: Tensor): Tensor = with(backend) { this@div.div(other) } + override fun Tensor.plus(other: Tensor): Tensor = + with(backend) { this@plus.plus(other) } + + override fun Tensor.minus(other: Tensor): Tensor = + with(backend) { this@minus.minus(other) } + + override fun Tensor.times(other: Tensor): Tensor = + with(backend) { this@times.times(other) } + + override fun Tensor.div(other: Tensor): Tensor = + with(backend) { this@div.div(other) } // Tensor-Scalar operations - delegate to backend override fun Tensor.plus(scalar: Int): Tensor = with(backend) { this@plus.plus(scalar) } - override fun Tensor.minus(scalar: Int): Tensor = with(backend) { this@minus.minus(scalar) } - override fun Tensor.times(scalar: Int): Tensor = with(backend) { this@times.times(scalar) } + override fun Tensor.minus(scalar: Int): Tensor = + with(backend) { this@minus.minus(scalar) } + + override fun Tensor.times(scalar: Int): Tensor = + with(backend) { this@times.times(scalar) } + override fun Tensor.div(scalar: Int): Tensor = with(backend) { this@div.div(scalar) } override fun Tensor.plus(scalar: Float): Tensor = with(backend) { this@plus.plus(scalar) } - override fun Tensor.minus(scalar: Float): Tensor = with(backend) { this@minus.minus(scalar) } - override fun Tensor.times(scalar: Float): Tensor = with(backend) { this@times.times(scalar) } + override fun Tensor.minus(scalar: Float): Tensor = + with(backend) { this@minus.minus(scalar) } + + override fun Tensor.times(scalar: Float): Tensor = + with(backend) { this@times.times(scalar) } + override fun Tensor.div(scalar: Float): Tensor = with(backend) { this@div.div(scalar) } - override fun Tensor.plus(scalar: Double): Tensor = with(backend) { this@plus.plus(scalar) } - override fun Tensor.minus(scalar: Double): Tensor = with(backend) { this@minus.minus(scalar) } - override fun Tensor.times(scalar: Double): Tensor = with(backend) { this@times.times(scalar) } + override fun Tensor.plus(scalar: Double): Tensor = + with(backend) { this@plus.plus(scalar) } + + override fun Tensor.minus(scalar: Double): Tensor = + with(backend) { this@minus.minus(scalar) } + + override fun Tensor.times(scalar: Double): Tensor = + with(backend) { this@times.times(scalar) } + override fun Tensor.div(scalar: Double): Tensor = with(backend) { this@div.div(scalar) } // Scalar-Tensor operations - delegate to backend diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt index d2eb92bcd..f1bff303c 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt @@ -1,6 +1,12 @@ -package sk.ai.net.core.tensor +package sk.ainet.core.tensor +import sk.ai.net.core.tensor.Shape import sk.ai.net.core.tensor.backend.* +import sk.ainet.core.tensor.backend.CpuTensorFP32 +import sk.ainet.core.tensor.backend.print +import sk.ainet.core.tensor.backend.printMatrix +import sk.ainet.core.tensor.backend.printScalar +import sk.ainet.core.tensor.backend.printVector import kotlin.test.* class PrintingTest { diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/TestUtils.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/TestUtils.kt index bab3d93a7..70f6da369 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/TestUtils.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/TestUtils.kt @@ -1,4 +1,4 @@ -package sk.ai.net.core.tensor +package sk.ainet.core.tensor import kotlin.test.assertTrue import kotlin.math.abs diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/backend/CpuBackendTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/backend/CpuBackendTest.kt index 09dbc04ab..30e9ee2b9 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/backend/CpuBackendTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/backend/CpuBackendTest.kt @@ -1,6 +1,6 @@ -package sk.ai.net.core.tensor.backend +package sk.ainet.core.tensor.backend -import sk.ai.net.core.tensor.* +import sk.ai.net.core.tensor.Shape import kotlin.test.* class CpuBackendTest { diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BaselinePerformanceTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BaselinePerformanceTest.kt index 402e614ab..77c6958b1 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BaselinePerformanceTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BaselinePerformanceTest.kt @@ -1,212 +1,52 @@ -package sk.ai.net.core.tensor.benchmark +package sk.ainet.core.tensor.benchmark -import sk.ai.net.core.tensor.backend.CpuBackend +import sk.ainet.core.performance.* import kotlin.test.Test /** - * Baseline performance test demonstrating the benchmarking framework. - * This test measures the current performance of CpuBackend operations - * to establish baseline metrics for SIMD comparison. + * Simple performance test demonstrating the extracted benchmarking framework. + * This validates that the performance module integration works correctly. */ class BaselinePerformanceTest { - private val backend = CpuBackend() - private val suite = TensorBenchmarkSuite(backend) + private val runner = BenchmarkRunner() @Test - fun testMatrixMultiplicationBaseline() { - println("\n=== MATRIX MULTIPLICATION BASELINE ===") + fun testBenchmarkRunnerIntegration() { + println("\n=== BENCHMARK RUNNER INTEGRATION TEST ===") - // Test smaller sizes for quick baseline measurement - val testSizes = listOf( - Triple(32, 32, 32), - Triple(64, 64, 64), - Triple(128, 128, 128), - Triple(256, 256, 256) - ) - - val results = suite.benchmarkMatrixMultiplication(testSizes) - - results.forEach { result -> - println(result.prettyPrint()) - println() - } - - // Validate performance characteristics - println("Performance Analysis:") - results.forEach { result -> - val size = extractMatrixSize(result.name) - val operations = size * size * size.toLong() // Approximate FLOPs - val flops = operations * result.throughput - println(" ${result.name}: ${formatFlops(flops)}") - } - } - - @Test - fun testElementwiseOperationsBaseline() { - println("\n=== ELEMENT-WISE OPERATIONS BASELINE ===") - - // Test moderate sizes for element-wise operations - val testSizes = listOf(1_000, 10_000, 100_000, 1_000_000) - - val results = suite.benchmarkElementwiseOps(testSizes) - - results.forEach { (operation, opResults) -> - println("$operation Operations:") - opResults.forEach { result -> - println(" ${result.prettyPrint()}") - } - println() - } - } - - @Test - fun testDotProductBaseline() { - println("\n=== DOT PRODUCT BASELINE ===") - - val testSizes = listOf(1_000, 10_000, 100_000, 1_000_000) - - val results = suite.benchmarkDotProduct(testSizes) - - results.forEach { result -> - println(result.prettyPrint()) - - // Calculate memory bandwidth estimation - val size = extractVectorSize(result.name) - val bytesProcessed = size * 2 * 4L // Two arrays of floats (4 bytes each) - val bandwidth = bytesProcessed * result.throughput / 1_000_000_000.0 // GB/s - println(" Estimated Memory Bandwidth: ${bandwidth.format(2)} GB/s") - println() - } - } - - @Test - fun testScalarOperationsBaseline() { - println("\n=== SCALAR OPERATIONS BASELINE ===") - - val testSizes = listOf(10_000, 100_000, 1_000_000) - - val results = suite.benchmarkScalarOps(testSizes) - - results.forEach { (operation, opResults) -> - println("$operation Operations:") - opResults.forEach { result -> - println(" ${result.prettyPrint()}") - } - println() - } - } - - @Test - fun testFullBenchmarkSuite() { - println("\n=== COMPREHENSIVE BASELINE BENCHMARK ===") - - val report = suite.runFullBenchmarkSuite() - println(report.prettyPrint()) - - // Performance insights - println("\nPERFORMACE INSIGHTS:") - - // Matrix multiplication scaling analysis - val matmulResults = report.matrixMultiplication.take(4) // First 4 square matrices - if (matmulResults.size >= 2) { - val small = matmulResults[0] - val large = matmulResults[3] - val sizeRatio = 8.0 // 256/32 = 8 - val timeRatio = large.executionTime.mean / small.executionTime.mean - val expectedRatio = sizeRatio * sizeRatio * sizeRatio // O(n³) - - println(" Matrix Multiplication Scaling:") - println(" Size ratio: ${sizeRatio}x") - println(" Time ratio: ${timeRatio.format(2)}x") - println(" Expected O(n³) ratio: ${expectedRatio.format(1)}x") - println(" Efficiency: ${(expectedRatio / timeRatio * 100).format(1)}%") - } - - // Element-wise operations should scale linearly - val addResults = report.elementwiseOperations["Add"] ?: emptyList() - if (addResults.size >= 2) { - val small = addResults[0] - val large = addResults.last() - val smallSize = extractVectorSize(small.name) - val largeSize = extractVectorSize(large.name) - val sizeRatio = largeSize.toDouble() / smallSize - val timeRatio = large.executionTime.mean / small.executionTime.mean - - println(" Element-wise Addition Scaling:") - println(" Size ratio: ${sizeRatio.format(1)}x") - println(" Time ratio: ${timeRatio.format(2)}x") - println(" Linearity: ${(sizeRatio / timeRatio * 100).format(1)}%") - } - } - - @Test - fun testMemoryUsageDemonstration() { - println("\n=== MEMORY USAGE ANALYSIS ===") - - // Note: Current implementation has placeholder memory stats - // This test demonstrates where real memory profiling would go - - val runner = BenchmarkRunner() - - // Large matrix multiplication to observe memory patterns + // Simple computation benchmark val result = runner.benchmark( - name = "Large Matrix Memory Test", - warmupRuns = 2, - measurementRuns = 5 + name = "Simple Array Sum", + warmupRuns = 3, + measurementRuns = 10 ) { - val backend = CpuBackend() - val a = createLargeMatrix(500, 500) - val b = createLargeMatrix(500, 500) - backend.matmul(a, b) + val array = IntArray(1000) { it } + array.sum() } println(result.prettyPrint()) - println("Note: Memory statistics are placeholders in current multiplatform implementation") - println("Real JVM implementation would show:") - println(" - Heap allocation patterns") - println(" - GC pressure") - println(" - Memory bandwidth utilization") + + // Verify the benchmark produces valid results + kotlin.test.assertTrue(result.throughput > 0, "Throughput should be positive") + kotlin.test.assertTrue(result.executionTime.mean > 0, "Mean execution time should be positive") + kotlin.test.assertEquals("Simple Array Sum", result.name) + + println("✓ Performance module integration successful") } - private fun createLargeMatrix(rows: Int, cols: Int) = - sk.ai.net.core.tensor.backend.CpuTensorFP32.fromArray( - sk.ai.net.core.tensor.Shape(rows, cols), - FloatArray(rows * cols) { (it % 100).toFloat() / 50f } + @Test + fun testWarmupWithDuration() { + println("\n=== WARMUP WITH DURATION TEST ===") + + var counter = 0 + runner.warmupWithDuration( + operation = { counter++ }, + minRuns = 5, + minTime = kotlin.time.Duration.parse("100ms") ) - - private fun extractMatrixSize(name: String): Int { - // Extract size from "MatMul 256x256 * 256x256" format - val regex = """MatMul (\d+)x\d+ \* \d+x\d+""".toRegex() - val match = regex.find(name) - return match?.groupValues?.get(1)?.toInt() ?: 32 - } - - private fun extractVectorSize(name: String): Int { - // Extract size from "DotProduct (1000000 elements)" format - val regex = """\((\d+) elements\)""".toRegex() - val match = regex.find(name) - return match?.groupValues?.get(1)?.toInt() ?: 1000 - } - - private fun formatFlops(flops: Double): String { - return when { - flops >= 1_000_000_000 -> "${(flops / 1_000_000_000).format(2)} GFLOP/s" - flops >= 1_000_000 -> "${(flops / 1_000_000).format(2)} MFLOP/s" - flops >= 1_000 -> "${(flops / 1_000).format(2)} KFLOP/s" - else -> "${flops.format(2)} FLOP/s" - } - } - - private fun Double.format(decimals: Int): String { - val multiplier = when (decimals) { - 0 -> 1.0 - 1 -> 10.0 - 2 -> 100.0 - 3 -> 1000.0 - else -> 1000.0 - } - val rounded = (this * multiplier).toInt() / multiplier - return rounded.toString() + + kotlin.test.assertTrue(counter >= 5, "Should run at least minRuns times") + println("✓ Warmup completed with $counter runs") } } \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BenchmarkRunner.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BenchmarkRunner.kt deleted file mode 100644 index 43a2c52f0..000000000 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/BenchmarkRunner.kt +++ /dev/null @@ -1,156 +0,0 @@ -package sk.ai.net.core.tensor.benchmark - -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds -import kotlin.time.measureTime -import kotlin.time.TimeSource - -/** - * Core benchmark runner for measuring tensor operation performance. - * Multiplatform-compatible implementation using Kotlin stdlib APIs. - */ -class BenchmarkRunner { - - /** - * Runs a comprehensive benchmark of the given operation. - * - * @param name Human-readable name for the benchmark - * @param warmupRuns Number of warmup iterations (default: 10) - * @param measurementRuns Number of measurement iterations (default: 100) - * @param operation The operation to benchmark - * @return Detailed benchmark results including timing statistics - */ - fun benchmark( - name: String, - warmupRuns: Int = 10, - measurementRuns: Int = 100, - operation: () -> T - ): BenchmarkResult { - // Warmup phase to allow JIT compilation - repeat(warmupRuns) { - operation() - } - - val times = mutableListOf() - - // Measurement phase - repeat(measurementRuns) { - val time = measureTime { operation() }.inWholeMicroseconds.toDouble() - times.add(time) - } - - return BenchmarkResult( - name = name, - executionTime = calculateTimeStatistics(times), - memoryUsage = MemoryStatistics(0L, 0L, 0, 0L), // Placeholder for memory stats - throughput = calculateThroughput(times), - metadata = mapOf( - "warmupRuns" to warmupRuns, - "measurementRuns" to measurementRuns, - "timestamp" to TimeSource.Monotonic.markNow().elapsedNow().inWholeMilliseconds - ) - ) - } - - /** - * Advanced warmup with time-based minimum duration - */ - fun warmupWithDuration( - operation: () -> Unit, - minRuns: Int = 10, - minTime: Duration = 5.seconds - ) { - val timeSource = TimeSource.Monotonic - val startMark = timeSource.markNow() - var runs = 0 - - while (runs < minRuns || startMark.elapsedNow() < minTime) { - operation() - runs++ - } - } - - private fun calculateTimeStatistics(times: List): TimeStatistics { - val sorted = times.sorted() - val mean = times.average() - val variance = times.map { (it - mean) * (it - mean) }.average() - val stdDev = kotlin.math.sqrt(variance) - - return TimeStatistics( - mean = mean, - median = sorted[sorted.size / 2], - standardDeviation = stdDev, - min = sorted.first(), - max = sorted.last(), - percentile95 = sorted[(sorted.size * 0.95).toInt()] - ) - } - - private fun calculateThroughput(times: List): Double { - val averageTimeMicros = times.average() - return 1_000_000.0 / averageTimeMicros // operations per second - } -} - -/** - * Comprehensive benchmark result containing timing statistics - */ -data class BenchmarkResult( - val name: String, - val executionTime: TimeStatistics, - val memoryUsage: MemoryStatistics, - val throughput: Double, // operations per second - val metadata: Map -) { - fun prettyPrint(): String { - return buildString { - appendLine("$name:") - appendLine(" Time: ${(executionTime.mean / 1000).format(3)}ms ± ${(executionTime.standardDeviation / 1000).format(3)}ms") - appendLine(" Throughput: ${throughput.format(1)} ops/sec") - appendLine(" Memory: ${formatBytes(memoryUsage.allocatedBytes)}") - appendLine(" Range: ${(executionTime.min / 1000).format(3)}ms - ${(executionTime.max / 1000).format(3)}ms") - } - } - - private fun formatBytes(bytes: Long): String { - return when { - bytes >= 1_000_000 -> "${(bytes / 1_000_000.0).format(1)}MB" - bytes >= 1_000 -> "${(bytes / 1_000.0).format(1)}KB" - else -> "${bytes}B" - } - } - - private fun Double.format(decimals: Int): String { - val multiplier = when (decimals) { - 0 -> 1.0 - 1 -> 10.0 - 2 -> 100.0 - 3 -> 1000.0 - else -> 1000.0 - } - val rounded = (this * multiplier).toInt() / multiplier - return rounded.toString() - } -} - -/** - * Statistical analysis of execution times - */ -data class TimeStatistics( - val mean: Double, // microseconds - val median: Double, // microseconds - val standardDeviation: Double, // microseconds - val min: Double, // microseconds - val max: Double, // microseconds - val percentile95: Double // microseconds -) - -/** - * Memory usage statistics (placeholder for multiplatform compatibility) - */ -data class MemoryStatistics( - val allocatedBytes: Long, - val peakHeapUsage: Long, - val gcCollections: Int, - val gcTime: Long // milliseconds -) \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/TensorBenchmarkSuite.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/TensorBenchmarkSuite.kt deleted file mode 100644 index 03ba02b25..000000000 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/benchmark/TensorBenchmarkSuite.kt +++ /dev/null @@ -1,347 +0,0 @@ -package sk.ai.net.core.tensor.benchmark - -import sk.ai.net.core.tensor.* -import sk.ai.net.core.tensor.backend.* - -/** - * Comprehensive benchmark suite for tensor operations. - * Tests performance of matrix multiplication, element-wise operations, and dot products - * across different tensor sizes and data patterns. - */ -class TensorBenchmarkSuite(private val backend: CpuBackend) { - - private val runner = BenchmarkRunner() - - /** - * Benchmark matrix multiplication across different sizes - */ - fun benchmarkMatrixMultiplication( - sizes: List> = defaultMatrixSizes - ): List { - return sizes.map { (rows, inner, cols) -> - val a = createRandomTensor(Shape(rows, inner)) - val b = createRandomTensor(Shape(inner, cols)) - - runner.benchmark( - name = "MatMul ${rows}x${inner} * ${inner}x${cols}", - warmupRuns = 5, - measurementRuns = 50 - ) { - backend.matmul(a, b) - } - } - } - - /** - * Benchmark element-wise operations (add, subtract, multiply, divide) - */ - fun benchmarkElementwiseOps( - sizes: List = defaultVectorSizes - ): Map> { - val operations = mapOf CpuTensorFP32>( - "Add" to { a, b -> with(backend) { a + b } as CpuTensorFP32 }, - "Subtract" to { a, b -> with(backend) { a - b } as CpuTensorFP32 }, - "Multiply" to { a, b -> with(backend) { a * b } as CpuTensorFP32 }, - "Divide" to { a, b -> with(backend) { a / b } as CpuTensorFP32 } - ) - - return operations.mapValues { (opName, operation) -> - sizes.map { size -> - val a = createRandomTensor(Shape(size)) - val b = createRandomTensor(Shape(size)) - - runner.benchmark( - name = "$opName (${size} elements)", - warmupRuns = 10, - measurementRuns = 100 - ) { - operation(a, b) - } - } - } - } - - /** - * Benchmark dot product operations - */ - fun benchmarkDotProduct( - sizes: List = defaultVectorSizes - ): List { - return sizes.map { size -> - val a = createRandomTensor(Shape(size)) - val b = createRandomTensor(Shape(size)) - - runner.benchmark( - name = "DotProduct (${size} elements)", - warmupRuns = 10, - measurementRuns = 100 - ) { - backend.dot(a, b) - } - } - } - - /** - * Benchmark scalar operations - */ - fun benchmarkScalarOps( - sizes: List = defaultVectorSizes - ): Map> { - val scalarOps = mapOf CpuTensorFP32>( - "Scale" to { tensor -> backend.scale(tensor, 2.5) as CpuTensorFP32 }, - "Add Scalar" to { tensor -> with(backend) { tensor + 3.14f } as CpuTensorFP32 }, - "Multiply Scalar" to { tensor -> with(backend) { tensor * 2.0f } as CpuTensorFP32 } - ) - - return scalarOps.mapValues { (opName, operation) -> - sizes.map { size -> - val tensor = createRandomTensor(Shape(size)) - - runner.benchmark( - name = "$opName (${size} elements)", - warmupRuns = 10, - measurementRuns = 100 - ) { - operation(tensor) - } - } - } - } - - /** - * Run comprehensive benchmark suite covering all operations - */ - fun runFullBenchmarkSuite(): BenchmarkReport { - println("Starting comprehensive tensor benchmark suite...") - - val matmulResults = benchmarkMatrixMultiplication() - val elementwiseResults = benchmarkElementwiseOps() - val dotProductResults = benchmarkDotProduct() - val scalarResults = benchmarkScalarOps() - - return BenchmarkReport( - backendName = backend.name, - matrixMultiplication = matmulResults, - elementwiseOperations = elementwiseResults, - dotProduct = dotProductResults, - scalarOperations = scalarResults, - summary = generateSummary(matmulResults, elementwiseResults, dotProductResults, scalarResults) - ) - } - - /** - * Compare performance between two backends (e.g., scalar vs SIMD) - */ - fun compareBackends( - otherBackend: CpuBackend, - otherBackendName: String - ): ComparisonReport { - println("Comparing performance between ${backend.name} and $otherBackendName...") - - val thisResults = runFullBenchmarkSuite() - val otherSuite = TensorBenchmarkSuite(otherBackend) - val otherResults = otherSuite.runFullBenchmarkSuite() - - return ComparisonReport( - baselineBackend = backend.name, - baselineResults = thisResults, - comparisonBackend = otherBackendName, - comparisonResults = otherResults, - speedupAnalysis = calculateSpeedups(thisResults, otherResults) - ) - } - - private fun createRandomTensor(shape: Shape): CpuTensorFP32 { - val size = shape.volume - val data = FloatArray(size) { (it % 100 + 1).toFloat() / 50f } // Values between 0.02 and 2.0 - return CpuTensorFP32.fromArray(shape, data) - } - - private fun generateSummary( - matmul: List, - elementwise: Map>, - dotProduct: List, - scalar: Map> - ): BenchmarkSummary { - val allResults = buildList { - addAll(matmul) - elementwise.values.forEach { addAll(it) } - addAll(dotProduct) - scalar.values.forEach { addAll(it) } - } - - return BenchmarkSummary( - totalOperations = allResults.size, - averageThroughput = allResults.map { it.throughput }.average(), - fastestOperation = allResults.maxByOrNull { it.throughput }?.name ?: "N/A", - slowestOperation = allResults.minByOrNull { it.throughput }?.name ?: "N/A" - ) - } - - private fun calculateSpeedups( - baseline: BenchmarkReport, - comparison: BenchmarkReport - ): SpeedupAnalysis { - val speedups = mutableMapOf() - - // Calculate speedups for matrix multiplication - baseline.matrixMultiplication.zip(comparison.matrixMultiplication) { base, comp -> - speedups[base.name] = base.executionTime.mean / comp.executionTime.mean - } - - // Calculate speedups for element-wise operations - baseline.elementwiseOperations.forEach { (opType, baseResults) -> - val compResults = comparison.elementwiseOperations[opType] ?: emptyList() - baseResults.zip(compResults) { base, comp -> - speedups[base.name] = base.executionTime.mean / comp.executionTime.mean - } - } - - val speedupValues = speedups.values.toList() - - return SpeedupAnalysis( - operationSpeedups = speedups, - averageSpeedup = speedupValues.average(), - bestCaseSpeedup = speedupValues.maxOrNull() ?: 1.0, - worstCaseSpeedup = speedupValues.minOrNull() ?: 1.0, - speedupByTensorSize = speedups // Simplified - could be grouped by size - ) - } - - companion object { - // Default benchmark sizes - val defaultMatrixSizes = listOf( - Triple(32, 32, 32), // Small - Triple(64, 64, 64), - Triple(128, 128, 128), // Medium - Triple(256, 256, 256), - Triple(512, 512, 512), // Large - Triple(1024, 1024, 1024), - Triple(100, 500, 200), // Rectangular - Triple(1000, 10, 1000), // Thin - Triple(10, 1000, 10) // Wide - ) - - val defaultVectorSizes = listOf( - 100, 1_000, 10_000, 100_000, 1_000_000 - ) - } -} - -/** - * Comprehensive benchmark report containing all operation results - */ -data class BenchmarkReport( - val backendName: String, - val matrixMultiplication: List, - val elementwiseOperations: Map>, - val dotProduct: List, - val scalarOperations: Map>, - val summary: BenchmarkSummary -) { - fun prettyPrint(): String { - return buildString { - appendLine("=" .repeat(50)) - appendLine("TENSOR BENCHMARK REPORT - $backendName") - appendLine("=" .repeat(50)) - appendLine() - - appendLine("MATRIX MULTIPLICATION:") - matrixMultiplication.forEach { result -> - appendLine(result.prettyPrint()) - } - appendLine() - - appendLine("ELEMENT-WISE OPERATIONS:") - elementwiseOperations.forEach { (opType, results) -> - appendLine(" $opType:") - results.forEach { result -> - appendLine(" ${result.prettyPrint().prependIndent(" ")}") - } - } - appendLine() - - appendLine("SUMMARY:") - appendLine(" Total Operations: ${summary.totalOperations}") - appendLine(" Average Throughput: ${summary.averageThroughput.format(1)} ops/sec") - appendLine(" Fastest: ${summary.fastestOperation}") - appendLine(" Slowest: ${summary.slowestOperation}") - } - } - - private fun Double.format(decimals: Int): String { - val multiplier = when (decimals) { - 0 -> 1.0 - 1 -> 10.0 - 2 -> 100.0 - 3 -> 1000.0 - else -> 1000.0 - } - val rounded = (this * multiplier).toInt() / multiplier - return rounded.toString() - } -} - -/** - * Summary statistics for a benchmark run - */ -data class BenchmarkSummary( - val totalOperations: Int, - val averageThroughput: Double, - val fastestOperation: String, - val slowestOperation: String -) - -/** - * Backend comparison report showing speedup analysis - */ -data class ComparisonReport( - val baselineBackend: String, - val baselineResults: BenchmarkReport, - val comparisonBackend: String, - val comparisonResults: BenchmarkReport, - val speedupAnalysis: SpeedupAnalysis -) { - fun prettyPrint(): String { - return buildString { - appendLine("=" .repeat(60)) - appendLine("BACKEND COMPARISON: $baselineBackend vs $comparisonBackend") - appendLine("=" .repeat(60)) - appendLine() - - appendLine("SPEEDUP ANALYSIS:") - appendLine(" Average Speedup: ${speedupAnalysis.averageSpeedup.format(2)}x") - appendLine(" Best Case: ${speedupAnalysis.bestCaseSpeedup.format(2)}x") - appendLine(" Worst Case: ${speedupAnalysis.worstCaseSpeedup.format(2)}x") - appendLine() - - appendLine("DETAILED SPEEDUPS:") - speedupAnalysis.operationSpeedups.forEach { (operation, speedup) -> - appendLine(" $operation: ${speedup.format(2)}x") - } - } - } - - private fun Double.format(decimals: Int): String { - val multiplier = when (decimals) { - 0 -> 1.0 - 1 -> 10.0 - 2 -> 100.0 - 3 -> 1000.0 - else -> 1000.0 - } - val rounded = (this * multiplier).toInt() / multiplier - return rounded.toString() - } -} - -/** - * Speedup analysis comparing two backends - */ -data class SpeedupAnalysis( - val operationSpeedups: Map, - val averageSpeedup: Double, - val bestCaseSpeedup: Double, - val worstCaseSpeedup: Double, - val speedupByTensorSize: Map -) \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/EdgeCasesTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/EdgeCasesTest.kt index 49e2305a2..3ba216383 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/EdgeCasesTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/EdgeCasesTest.kt @@ -1,7 +1,8 @@ -package sk.ai.net.core.tensor.multiplication +package sk.ainet.core.tensor.multiplication import sk.ai.net.core.tensor.* -import sk.ai.net.core.tensor.backend.* +import sk.ainet.core.tensor.backend.CpuBackend +import sk.ainet.core.tensor.backend.CpuTensorFP32 import kotlin.test.* class EdgeCasesTest { diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/ElementwiseMultiplicationTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/ElementwiseMultiplicationTest.kt index 69ebf4582..afca23bd1 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/ElementwiseMultiplicationTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/ElementwiseMultiplicationTest.kt @@ -1,7 +1,8 @@ -package sk.ai.net.core.tensor.multiplication +package sk.ainet.core.tensor.multiplication import sk.ai.net.core.tensor.* -import sk.ai.net.core.tensor.backend.* +import sk.ainet.core.tensor.backend.CpuBackend +import sk.ainet.core.tensor.backend.CpuTensorFP32 import kotlin.test.* class ElementwiseMultiplicationTest { diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MLOperationsTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MLOperationsTest.kt index cf82958d7..f317653f0 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MLOperationsTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MLOperationsTest.kt @@ -1,7 +1,8 @@ -package sk.ai.net.core.tensor.multiplication +package sk.ainet.core.tensor.multiplication import sk.ai.net.core.tensor.* -import sk.ai.net.core.tensor.backend.* +import sk.ainet.core.tensor.backend.CpuBackend +import sk.ainet.core.tensor.backend.CpuTensorFP32 import kotlin.test.* class MLOperationsTest { @@ -327,7 +328,7 @@ class MLOperationsTest { with(backend) { activation = activation.plus(biases[0]) } - + // Apply ReLU activation (max(0, x)) val relu1Data = FloatArray(activation.shape.volume) for (i in relu1Data.indices) { @@ -341,7 +342,7 @@ class MLOperationsTest { with(backend) { activation = activation.plus(biases[1]) } - + // Apply ReLU activation val relu2Data = FloatArray(activation.shape.volume) for (i in relu2Data.indices) { @@ -355,25 +356,25 @@ class MLOperationsTest { // Verify MLP structure assertEquals(3, weights.size, "Should have 3 weight matrices") assertEquals(3, biases.size, "Should have 3 bias vectors") - + // Verify layer shapes assertEquals(Shape(inputSize, hiddenSize), weights[0].shape, "Layer 1 weight shape") - assertEquals(Shape(hiddenSize, hiddenSize), weights[1].shape, "Layer 2 weight shape") + assertEquals(Shape(hiddenSize, hiddenSize), weights[1].shape, "Layer 2 weight shape") assertEquals(Shape(hiddenSize, outputSize), weights[2].shape, "Layer 3 weight shape") - + // Verify output shape assertEquals(Shape(batchSize, outputSize), output.shape, "Output should be batch_size x output_size") - + // Verify output values are reasonable (not NaN or infinite) for (i in 0 until batchSize) { val outputValue = output[i, 0] assertTrue(outputValue.isFinite(), "Output should be finite") println("[DEBUG_LOG] Sample $i: input=${inputData[i]}, output=$outputValue") } - + // Verify different inputs produce different outputs assertNotEquals(output[0, 0], output[1, 0], "Different inputs should produce different outputs") - + println("[DEBUG_LOG] MLP test completed successfully with ${weights.size} layers") } } \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MatrixMultiplicationTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MatrixMultiplicationTest.kt index cf15dcfe2..e672c67dc 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MatrixMultiplicationTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/multiplication/MatrixMultiplicationTest.kt @@ -1,25 +1,27 @@ -package sk.ai.net.core.tensor.multiplication +package sk.ainet.core.tensor.multiplication import sk.ai.net.core.tensor.* -import sk.ai.net.core.tensor.backend.* +import sk.ainet.core.tensor.backend.CpuBackend +import sk.ainet.core.tensor.backend.CpuTensorFP32 +import sk.ainet.core.tensor.backend.TensorFP32 import kotlin.test.* class MatrixMultiplicationTest { - + private val backend = CpuBackend() - + @Test fun testBasicMatrixMultiplication() { val shape1 = Shape(2, 3) val shape2 = Shape(3, 2) val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) // 2x3 matrix val data2 = floatArrayOf(7f, 8f, 9f, 10f, 11f, 12f) // 3x2 matrix - + val tensor1: TensorFP32 = CpuTensorFP32.fromArray(shape1, data1) val tensor2: TensorFP32 = CpuTensorFP32.fromArray(shape2, data2) - + val result = backend.matmul(tensor1, tensor2) - + assertEquals(Shape(2, 2), result.shape) // First row: [1*7 + 2*9 + 3*11, 1*8 + 2*10 + 3*12] = [58, 64] // Second row: [4*7 + 5*9 + 6*11, 4*8 + 5*10 + 6*12] = [139, 154] @@ -28,18 +30,18 @@ class MatrixMultiplicationTest { assertEquals(139f, result[1, 0]) assertEquals(154f, result[1, 1]) } - + @Test fun testSquareMatrixMultiplication() { val shape = Shape(3, 3) val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f) val data2 = floatArrayOf(9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f) - + val tensor1: TensorFP32 = CpuTensorFP32.fromArray(shape, data1) val tensor2: TensorFP32 = CpuTensorFP32.fromArray(shape, data2) - + val result = backend.matmul(tensor1, tensor2) - + assertEquals(shape, result.shape) // First row: [1*9 + 2*6 + 3*3, 1*8 + 2*5 + 3*2, 1*7 + 2*4 + 3*1] = [30, 24, 18] assertEquals(30f, result[0, 0]) @@ -54,18 +56,18 @@ class MatrixMultiplicationTest { assertEquals(114f, result[2, 1]) assertEquals(90f, result[2, 2]) } - + @Test fun testIdentityMatrixMultiplication() { val shape = Shape(3, 3) val data = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f) val identityData = floatArrayOf(1f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f) - + val tensor = CpuTensorFP32.fromArray(shape, data) val identity = CpuTensorFP32.fromArray(shape, identityData) - + val result = backend.matmul(tensor, identity) - + assertEquals(shape, result.shape) // Multiplying by identity should return the original matrix assertEquals(1f, result[0, 0]) @@ -78,7 +80,7 @@ class MatrixMultiplicationTest { assertEquals(8f, result[2, 1]) assertEquals(9f, result[2, 2]) } - + @Test fun testVectorMatrixMultiplication() { // 1x3 vector * 3x4 matrix = 1x4 vector @@ -86,12 +88,12 @@ class MatrixMultiplicationTest { val shape2 = Shape(3, 4) val data1 = floatArrayOf(2f, 3f, 4f) // row vector val data2 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f) // 3x4 matrix - + val vector = CpuTensorFP32.fromArray(shape1, data1) val matrix = CpuTensorFP32.fromArray(shape2, data2) - + val result = backend.matmul(vector, matrix) - + assertEquals(Shape(1, 4), result.shape) // [2*1 + 3*5 + 4*9, 2*2 + 3*6 + 4*10, 2*3 + 3*7 + 4*11, 2*4 + 3*8 + 4*12] = [53, 62, 71, 80] assertEquals(53f, result[0, 0]) @@ -99,7 +101,7 @@ class MatrixMultiplicationTest { assertEquals(71f, result[0, 2]) assertEquals(80f, result[0, 3]) } - + @Test fun testMatrixVectorMultiplication() { // 4x3 matrix * 3x1 vector = 4x1 vector @@ -107,12 +109,12 @@ class MatrixMultiplicationTest { val shape2 = Shape(3, 1) val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 11f, 12f) // 4x3 matrix val data2 = floatArrayOf(2f, 3f, 4f) // column vector - + val matrix = CpuTensorFP32.fromArray(shape1, data1) val vector = CpuTensorFP32.fromArray(shape2, data2) - + val result = backend.matmul(matrix, vector) - + assertEquals(Shape(4, 1), result.shape) // [1*2 + 2*3 + 3*4] = [20], [4*2 + 5*3 + 6*4] = [47], [7*2 + 8*3 + 9*4] = [74], [10*2 + 11*3 + 12*4] = [101] assertEquals(20f, result[0, 0]) @@ -120,55 +122,55 @@ class MatrixMultiplicationTest { assertEquals(74f, result[2, 0]) assertEquals(101f, result[3, 0]) } - + @Test fun testLargeMatrixMultiplication() { val shape1 = Shape(50, 30) val shape2 = Shape(30, 40) val data1 = FloatArray(1500) { (it % 10 + 1).toFloat() } val data2 = FloatArray(1200) { (it % 5 + 1).toFloat() } - + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) val tensor2 = CpuTensorFP32.fromArray(shape2, data2) - + val result = backend.matmul(tensor1, tensor2) - + assertEquals(Shape(50, 40), result.shape) // Verify specific elements by manual calculation assertTrue(result[0, 0] > 0f) assertTrue(result[49, 39] > 0f) } - + @Test fun testMatrixMultiplicationWithZeros() { val shape1 = Shape(2, 3) val shape2 = Shape(3, 2) val data1 = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) val zeros = CpuTensorFP32.zeros(shape2) - + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) - + val result = backend.matmul(tensor1, zeros) - + assertEquals(Shape(2, 2), result.shape) assertEquals(0f, result[0, 0]) assertEquals(0f, result[0, 1]) assertEquals(0f, result[1, 0]) assertEquals(0f, result[1, 1]) } - + @Test fun testMatrixMultiplicationWithNegatives() { val shape1 = Shape(2, 2) val shape2 = Shape(2, 2) val data1 = floatArrayOf(-1f, 2f, -3f, 4f) val data2 = floatArrayOf(5f, -6f, -7f, 8f) - + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) val tensor2 = CpuTensorFP32.fromArray(shape2, data2) - + val result = backend.matmul(tensor1, tensor2) - + assertEquals(shape1, result.shape) // First row: [-1*5 + 2*(-7), -1*(-6) + 2*8] = [-19, 22] // Second row: [-3*5 + 4*(-7), -3*(-6) + 4*8] = [-43, 50] @@ -177,22 +179,22 @@ class MatrixMultiplicationTest { assertEquals(-43f, result[1, 0]) assertEquals(50f, result[1, 1]) } - + @Test fun testInvalidMatrixMultiplicationDimensions() { val shape1 = Shape(2, 3) val shape2 = Shape(4, 2) // Wrong inner dimension - should be 3x2 val data1 = FloatArray(6) { it.toFloat() } val data2 = FloatArray(8) { it.toFloat() } - + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) val tensor2 = CpuTensorFP32.fromArray(shape2, data2) - + assertFailsWith { backend.matmul(tensor1, tensor2) } } - + @Test fun testMatrixMultiplicationNonSquareResult() { // Test rectangular result matrices @@ -200,36 +202,36 @@ class MatrixMultiplicationTest { val shape2 = Shape(3, 7) val data1 = FloatArray(15) { (it + 1).toFloat() } val data2 = FloatArray(21) { (it % 3 + 1).toFloat() } - + val tensor1 = CpuTensorFP32.fromArray(shape1, data1) val tensor2 = CpuTensorFP32.fromArray(shape2, data2) - + val result = backend.matmul(tensor1, tensor2) - + assertEquals(Shape(5, 7), result.shape) // Verify the multiplication produces correct dimensions assertTrue(result[0, 0] > 0f) assertTrue(result[4, 6] > 0f) } - + @Test fun testMatrixChainMultiplication() { // Test A * B * C through (A * B) * C val shapeA = Shape(2, 3) val shapeB = Shape(3, 4) val shapeC = Shape(4, 2) - + val dataA = floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f) val dataB = FloatArray(12) { (it + 1).toFloat() } val dataC = FloatArray(8) { (it % 2 + 1).toFloat() } - + val tensorA = CpuTensorFP32.fromArray(shapeA, dataA) val tensorB = CpuTensorFP32.fromArray(shapeB, dataB) val tensorC = CpuTensorFP32.fromArray(shapeC, dataC) - + val resultAB = backend.matmul(tensorA, tensorB) val resultABC = backend.matmul(resultAB, tensorC) - + assertEquals(Shape(2, 2), resultABC.shape) assertTrue(resultABC[0, 0] > 0f) assertTrue(resultABC[1, 1] > 0f) From 7f1ab123f9268903ebda3d8a33fd876c9e11dbce Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 13:17:16 +0200 Subject: [PATCH 12/16] Fix package names to be sk.ainet also in build file --- skainet-core/skainet-tensors/build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skainet-core/skainet-tensors/build.gradle.kts b/skainet-core/skainet-tensors/build.gradle.kts index 79eb06b1e..ef281ade0 100644 --- a/skainet-core/skainet-tensors/build.gradle.kts +++ b/skainet-core/skainet-tensors/build.gradle.kts @@ -41,12 +41,13 @@ kotlin { commonTest.dependencies { implementation(libs.kotlin.test) + implementation(project(":skainet-core:skainet-performance")) } } } android { - namespace = "sk.ai.net.core.api" + namespace = "sk.ainet.core.api" compileSdk = libs.versions.android.compileSdk.get().toInt() defaultConfig { From 892ccb94ce596709341584c31d0c4d565ff675a7 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 13:17:37 +0200 Subject: [PATCH 13/16] Add comments --- .../kotlin/sk/ainet/core/tensor/TensorOps.kt | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorOps.kt b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorOps.kt index 013068673..fd7fadc19 100644 --- a/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorOps.kt +++ b/skainet-core/skainet-tensors-api/src/commonMain/kotlin/sk/ainet/core/tensor/TensorOps.kt @@ -10,11 +10,28 @@ public interface TensorOps> { /** * Performs matrix multiplication of two tensors. * - * @param other The tensor to multiply with. + * @param a The first tensor. + * @param b The second tensor. * @return The result of matrix multiplication. */ public fun matmul(a: T, b: T): T + + /** + * Scales a tensor by a scalar value. + * + * @param a The tensor to scale. + * @param scalar The scalar value to scale by. + * @return The scaled tensor. + */ public fun scale(a: T, scalar: Double): T + + /** + * Computes the dot product of two tensors. + * + * @param a The first tensor. + * @param b The second tensor. + * @return The dot product as a Double. + */ public fun dot(a: T, b: T): Double // Tensor–Tensor @@ -25,7 +42,15 @@ public interface TensorOps> { * @return The result of adding the two tensors. */ public operator fun T.plus(other: T): T + + /** + * Subtracts one tensor from another element-wise. + * + * @param other The tensor to subtract. + * @return The result of subtracting the two tensors. + */ public operator fun T.minus(other: T): T + /** * Multiplies two tensors element-wise. * @@ -33,6 +58,13 @@ public interface TensorOps> { * @return The result of multiplying the two tensors. */ public operator fun T.times(other: T): T // elementwise multiply + + /** + * Divides one tensor by another element-wise. + * + * @param other The tensor to divide by. + * @return The result of dividing the two tensors. + */ public operator fun T.div(other: T): T // elementwise divide // Tensor–Scalar From f5b2926fa40747a23ed2146dccce4cf202fd666f Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 13:18:02 +0200 Subject: [PATCH 14/16] Add docs on perfomance --- docs/modules/arc42/nav.adoc | 15 +- docs/modules/getting-started/nav.adoc | 1 - .../getting-started/pages/installation.adoc | 8 +- .../pages/benchmark-framework.adoc | 281 ++++++++ .../performance/pages/metrics-analysis.adoc | 470 +++++++++++++ .../pages/performance-testing.adoc | 647 ++++++++++++++++++ 6 files changed, 1415 insertions(+), 7 deletions(-) create mode 100644 docs/modules/performance/pages/benchmark-framework.adoc create mode 100644 docs/modules/performance/pages/metrics-analysis.adoc create mode 100644 docs/modules/performance/pages/performance-testing.adoc diff --git a/docs/modules/arc42/nav.adoc b/docs/modules/arc42/nav.adoc index 32b6d9152..66279683c 100644 --- a/docs/modules/arc42/nav.adoc +++ b/docs/modules/arc42/nav.adoc @@ -1,3 +1,14 @@ -= SKaiNET += Skainet Architecture Documentation -* xref:index.adoc[Introduction and Goals] +* xref:01-introduction-and-goals.adoc[Introduction and Goals] +* xref:02-architecture-constraints.adoc[Architecture Constraints] +* xref:03-context-and-scope.adoc[Context and Scope] +* xref:04-solution-strategy.adoc[Solution Strategy] +* xref:05-building-block-view.adoc[Building Block View] +* xref:06-runtime-view.adoc[Runtime View] +* xref:07-deployment-view.adoc[Deployment View] +* xref:08-crosscutting-concepts.adoc[Crosscutting Concepts] +* xref:09-architecture-decisions.adoc[Architecture Decisions] +* xref:10-quality-requirements.adoc[Quality Requirements] +* xref:11-risks-and-technical-debt.adoc[Risks and Technical Debt] +* xref:12-glossary.adoc[Glossary] \ No newline at end of file diff --git a/docs/modules/getting-started/nav.adoc b/docs/modules/getting-started/nav.adoc index d17b2ed4b..7088062a2 100644 --- a/docs/modules/getting-started/nav.adoc +++ b/docs/modules/getting-started/nav.adoc @@ -4,7 +4,6 @@ * xref:installation.adoc[Installation & Setup] * xref:basic-tensors.adoc[Basic Tensor Operations] * xref:matrix-operations.adoc[Matrix Operations] -* xref:linear-algebra.adoc[Linear Algebra Examples] * xref:neural-network-basics.adoc[Neural Network Basics] * xref:data-processing.adoc[Data Processing Use Cases] * xref:performance-tips.adoc[Performance Tips] \ No newline at end of file diff --git a/docs/modules/getting-started/pages/installation.adoc b/docs/modules/getting-started/pages/installation.adoc index 3b4d7298f..2881c5a94 100644 --- a/docs/modules/getting-started/pages/installation.adoc +++ b/docs/modules/getting-started/pages/installation.adoc @@ -58,8 +58,8 @@ Create `src/commonMain/kotlin/Main.kt` with this simple example: [source,kotlin] ---- -import sk.ai.net.core.tensor.* -import sk.ai.net.core.tensor.backend.* +import sk.ainet.core.tensor.* +import sk.ainet.core.tensor.backend.* fun main() { // Initialize the CPU backend @@ -111,10 +111,10 @@ For most SKaiNET operations, you'll need these imports: [source,kotlin] ---- // Core tensor interfaces and data types -import sk.ai.net.core.tensor.* +import sk.ainet.core.tensor.* // Backend implementations -import sk.ai.net.core.tensor.backend.* +import sk.ainet.core.tensor.backend.* ---- == Development Environment Tips diff --git a/docs/modules/performance/pages/benchmark-framework.adoc b/docs/modules/performance/pages/benchmark-framework.adoc new file mode 100644 index 000000000..02b8782f1 --- /dev/null +++ b/docs/modules/performance/pages/benchmark-framework.adoc @@ -0,0 +1,281 @@ += Benchmark Framework +:toc: +:toclevels: 3 +:sectanchors: +:sectlinks: + +The SKaiNET Benchmark Framework provides a comprehensive, multiplatform-compatible solution for measuring tensor operation performance. Built on Kotlin's standard library, it offers statistical analysis, warmup strategies, and detailed reporting capabilities. + +== Overview + +The benchmark framework consists of core components designed for accurate performance measurement: + +* **BenchmarkRunner**: Main execution engine for performance tests +* **BenchmarkResult**: Comprehensive result data structures +* **Statistical Analysis**: Time statistics, memory usage, and throughput calculations +* **Reporting**: Pretty-printed results and comparison reports +* **Multiplatform Support**: Compatible across JVM, Native, and JS targets + +== Core Components + +=== BenchmarkRunner + +The `BenchmarkRunner` is the primary interface for executing performance measurements. It handles warmup phases, measurement iterations, and statistical calculations. + +[source,kotlin] +---- +val runner = BenchmarkRunner() +val result = runner.benchmark( + name = "Matrix Multiplication 512x512", + warmupRuns = 10, + measurementRuns = 100 +) { + backend.matmul(matrixA, matrixB) +} +---- + +==== Key Methods + +**benchmark()**:: +Executes a complete benchmark cycle with warmup and measurement phases. + +**warmupWithDuration()**:: +Advanced warmup with time-based minimum duration for more consistent results. + +[source,kotlin] +---- +runner.warmupWithDuration( + operation = { heavyComputation() }, + minRuns = 10, + minTime = 5.seconds +) +---- + +=== BenchmarkResult Structure + +The framework returns comprehensive results containing multiple metrics: + +[source,kotlin] +---- +data class BenchmarkResult( + val name: String, + val executionTime: TimeStatistics, + val memoryUsage: MemoryStatistics, + val throughput: Double, // operations per second + val metadata: Map +) +---- + +==== TimeStatistics + +Detailed timing analysis including: + +* **Mean**: Average execution time in microseconds +* **Median**: Middle value of sorted execution times +* **Standard Deviation**: Measure of timing variability +* **Min/Max**: Fastest and slowest execution times +* **95th Percentile**: Performance threshold for outlier detection + +==== MemoryStatistics + +Memory usage tracking (platform-dependent): + +* **Allocated Bytes**: Total memory allocated during operation +* **Peak Heap Usage**: Maximum heap memory consumption +* **GC Collections**: Number of garbage collection cycles +* **GC Time**: Total time spent in garbage collection + +== Advanced Features + +=== Warmup Strategies + +The framework provides multiple warmup approaches: + +**Fixed Iterations**:: +Traditional approach with a fixed number of warmup runs. + +**Duration-Based**:: +Ensures minimum warmup time for consistent JIT compilation effects. + +**Adaptive**:: +Automatically determines optimal warmup based on timing stability. + +=== Statistical Analysis + +Built-in statistical calculations provide: + +* **Outlier Detection**: Identifies and handles performance anomalies +* **Confidence Intervals**: Statistical confidence in measurements +* **Throughput Calculations**: Operations per second based on timing data +* **Variance Analysis**: Measure of performance consistency + +=== Reporting and Visualization + +==== Pretty Printing + +Results include formatted output for easy interpretation: + +[source,kotlin] +---- +println(result.prettyPrint()) +// Output: +// Matrix Multiplication 512x512: +// Time: 2.45ms ± 0.12ms +// Throughput: 408.2 ops/sec +// Memory: 2.1MB +// Range: 2.31ms - 2.78ms +---- + +==== Comprehensive Reports + +Generate detailed reports for multiple operations: + +[source,kotlin] +---- +val report = BenchmarkReport( + backendName = "CPU Backend", + matrixMultiplication = listOf(result1, result2), + elementwiseOperations = mapOf( + "Addition" to listOf(addResult), + "Multiplication" to listOf(mulResult) + ), + // ... other operations +) +---- + +== Best Practices + +=== Measurement Guidelines + +**Warmup Phase**:: +Always include adequate warmup to allow JIT compilation and cache warming. + +**Sample Size**:: +Use sufficient measurement runs (100-1000) for statistical significance. + +**Environment Control**:: +Run benchmarks in consistent environments, avoiding background processes. + +**Multiple Iterations**:: +Execute benchmark suites multiple times and analyze result stability. + +=== Code Examples + +==== Basic Benchmark + +[source,kotlin] +---- +val runner = BenchmarkRunner() + +// Simple operation benchmark +val result = runner.benchmark( + name = "Element-wise Addition", + warmupRuns = 5, + measurementRuns = 50 +) { + tensorA + tensorB +} + +println("Throughput: ${result.throughput} ops/sec") +---- + +==== Backend Comparison + +[source,kotlin] +---- +val cpuResult = benchmarkBackend(cpuBackend, "CPU") +val gpuResult = benchmarkBackend(gpuBackend, "GPU") + +val comparison = ComparisonReport( + baselineBackend = "CPU", + baselineResults = cpuResult, + comparisonBackend = "GPU", + comparisonResults = gpuResult, + speedupAnalysis = calculateSpeedup(cpuResult, gpuResult) +) + +println(comparison.prettyPrint()) +---- + +==== Matrix Size Scaling + +[source,kotlin] +---- +val sizes = listOf(64, 128, 256, 512, 1024) +val results = mutableMapOf() + +sizes.forEach { size -> + val matrixA = createRandomMatrix(size, size) + val matrixB = createRandomMatrix(size, size) + + results[size] = runner.benchmark( + name = "MatMul ${size}x${size}", + measurementRuns = 20 + ) { + backend.matmul(matrixA, matrixB) + } +} + +// Analyze scaling behavior +results.forEach { (size, result) -> + println("${size}x${size}: ${result.executionTime.mean/1000}ms") +} +---- + +== Integration + +=== Dependency Configuration + +Add the performance module to your test dependencies: + +[source,kotlin] +---- +// build.gradle.kts +dependencies { + testImplementation("sk.ainet.core:skainet-performance") +} +---- + +=== Module Structure + +The performance module is organized as: + +``` +skainet-core/skainet-performance/ +├── src/commonMain/kotlin/sk/ainet/core/performance/ +│ ├── BenchmarkRunner.kt # Core benchmark execution +│ └── BenchmarkResult.kt # Result data structures +└── src/commonTest/kotlin/ # Framework tests +``` + +=== Multiplatform Compatibility + +The framework uses only Kotlin stdlib APIs for maximum compatibility: + +* **JVM**: Full functionality including memory statistics +* **Native**: Core timing and throughput measurements +* **JS**: Basic performance measurement capabilities + +== Troubleshooting + +=== Common Issues + +**Inconsistent Results**:: +Increase warmup runs or use duration-based warmup for JIT stability. + +**High Variance**:: +Check for background processes, increase measurement runs, or use median instead of mean. + +**Memory Measurement**:: +Memory statistics may be limited or unavailable on some platforms. + +**Platform Differences**:: +Timing precision varies between platforms; adjust measurement runs accordingly. + +=== Performance Tips + +* Use release builds for accurate performance measurements +* Disable debug logging during benchmarking +* Close other applications to reduce system noise +* Run multiple benchmark sessions and compare results +* Monitor system resources during long benchmark runs \ No newline at end of file diff --git a/docs/modules/performance/pages/metrics-analysis.adoc b/docs/modules/performance/pages/metrics-analysis.adoc new file mode 100644 index 000000000..e61c954a5 --- /dev/null +++ b/docs/modules/performance/pages/metrics-analysis.adoc @@ -0,0 +1,470 @@ += Metrics and Analysis +:toc: +:toclevels: 3 +:sectanchors: +:sectlinks: + +This guide covers the comprehensive analysis of performance metrics in SKaiNET, including statistical analysis, trend identification, bottleneck detection, and actionable insights for optimization. + +== Overview + +Effective performance analysis transforms raw benchmark data into actionable insights. This guide covers: + +* **Metric Types**: Understanding different performance measurements +* **Statistical Analysis**: Interpreting timing and throughput data +* **Trend Analysis**: Identifying performance patterns over time +* **Comparative Analysis**: Backend and algorithm comparisons +* **Bottleneck Detection**: Identifying performance constraints +* **Optimization Guidance**: Data-driven performance improvements + +== Core Performance Metrics + +=== Timing Metrics + +==== Execution Time Statistics + +[source,kotlin] +---- +data class TimeStatistics( + val mean: Double, // Average execution time (μs) + val median: Double, // 50th percentile (μs) + val standardDeviation: Double, // Timing variability + val min: Double, // Fastest execution (μs) + val max: Double, // Slowest execution (μs) + val percentile95: Double // 95th percentile (μs) +) +---- + +**Mean vs Median**:: +Mean provides overall average; median better represents typical performance when outliers are present. + +**Standard Deviation**:: +Measures consistency; lower values indicate more predictable performance. + +**Percentiles**:: +95th percentile shows performance threshold that most operations achieve. + +=== Throughput Metrics + +**Operations Per Second (OPS)**:: +Primary metric for batch processing scenarios. + +[source,kotlin] +---- +fun calculateThroughput(executionTimes: List): Double { + val averageTimeMicros = executionTimes.average() + return 1_000_000.0 / averageTimeMicros // ops/sec +} +---- + +**Sustained Throughput**:: +Long-term throughput accounting for system effects like garbage collection. + +**Peak Throughput**:: +Maximum throughput achieved during optimal conditions. + +=== Resource Utilization Metrics + +==== Memory Metrics + +[source,kotlin] +---- +data class MemoryStatistics( + val allocatedBytes: Long, // Total memory allocated + val peakHeapUsage: Long, // Maximum heap consumption + val gcCollections: Int, // GC cycle count + val gcTime: Long // Time spent in GC (ms) +) +---- + +==== Computational Efficiency + +**FLOPS (Floating Point Operations Per Second)**:: +Measures computational throughput for mathematical operations. + +**Memory Bandwidth Utilization**:: +Percentage of theoretical memory bandwidth achieved. + +**Cache Hit Rates**:: +L1/L2/L3 cache effectiveness for memory access patterns. + +== Statistical Analysis Techniques + +=== Descriptive Statistics + +==== Central Tendency Analysis + +[source,kotlin] +---- +fun analyzeTimingDistribution(times: List): TimingAnalysis { + val sorted = times.sorted() + val mean = times.average() + val median = sorted[sorted.size / 2] + val mode = times.groupBy { it }.maxByOrNull { it.value.size }?.key + + return TimingAnalysis( + mean = mean, + median = median, + mode = mode, + skewness = calculateSkewness(times, mean), + kurtosis = calculateKurtosis(times, mean) + ) +} +---- + +==== Variance and Distribution Analysis + +**Coefficient of Variation (CV)**:: +Relative variability measure: `CV = σ/μ` + +[source,kotlin] +---- +fun calculateCoeffientOfVariation(times: List): Double { + val mean = times.average() + val stdDev = calculateStandardDeviation(times, mean) + return stdDev / mean +} +---- + +**Distribution Shape**:: +- Skewness: Asymmetry in timing distribution +- Kurtosis: Tail heaviness indicating outlier frequency + +=== Outlier Detection + +==== Statistical Outlier Identification + +[source,kotlin] +---- +fun detectOutliers(times: List): OutlierAnalysis { + val sorted = times.sorted() + val q1 = sorted[(sorted.size * 0.25).toInt()] + val q3 = sorted[(sorted.size * 0.75).toInt()] + val iqr = q3 - q1 + + val lowerBound = q1 - 1.5 * iqr + val upperBound = q3 + 1.5 * iqr + + val outliers = times.filter { it < lowerBound || it > upperBound } + + return OutlierAnalysis( + outlierCount = outliers.size, + outlierPercentage = outliers.size.toDouble() / times.size * 100, + outliers = outliers, + cleanedMean = times.filter { it >= lowerBound && it <= upperBound }.average() + ) +} +---- + +==== Performance Anomaly Detection + +**Z-Score Method**:: +Identifies measurements beyond statistical thresholds. + +**Modified Z-Score**:: +More robust for non-normal distributions using median absolute deviation. + +=== Confidence Intervals + +==== Statistical Confidence + +[source,kotlin] +---- +fun calculateConfidenceInterval( + times: List, + confidenceLevel: Double = 0.95 +): ConfidenceInterval { + val mean = times.average() + val stdError = calculateStandardError(times) + val tValue = getTValue(times.size - 1, confidenceLevel) + + val marginOfError = tValue * stdError + + return ConfidenceInterval( + lowerBound = mean - marginOfError, + upperBound = mean + marginOfError, + confidenceLevel = confidenceLevel + ) +} +---- + +== Comparative Analysis + +=== Backend Performance Comparison + +==== Speedup Analysis + +[source,kotlin] +---- +fun calculateSpeedup( + baselineResults: List, + optimizedResults: List +): SpeedupAnalysis { + val speedups = baselineResults.zip(optimizedResults).map { (baseline, optimized) -> + baseline.throughput / optimized.throughput + } + + return SpeedupAnalysis( + operationSpeedups = speedups.mapIndexed { i, speedup -> + baselineResults[i].name to speedup + }.toMap(), + averageSpeedup = speedups.average(), + geometricMeanSpeedup = calculateGeometricMean(speedups), + bestCaseSpeedup = speedups.maxOrNull() ?: 0.0, + worstCaseSpeedup = speedups.minOrNull() ?: 0.0 + ) +} +---- + +==== Statistical Significance Testing + +[source,kotlin] +---- +fun performTTest( + sample1: List, + sample2: List +): TTestResult { + val mean1 = sample1.average() + val mean2 = sample2.average() + val variance1 = calculateVariance(sample1) + val variance2 = calculateVariance(sample2) + + val pooledVariance = ((sample1.size - 1) * variance1 + (sample2.size - 1) * variance2) / + (sample1.size + sample2.size - 2) + + val standardError = sqrt(pooledVariance * (1.0/sample1.size + 1.0/sample2.size)) + val tStatistic = (mean1 - mean2) / standardError + val pValue = calculatePValue(tStatistic, sample1.size + sample2.size - 2) + + return TTestResult( + tStatistic = tStatistic, + pValue = pValue, + isSignificant = pValue < 0.05, + effectSize = (mean1 - mean2) / sqrt(pooledVariance) + ) +} +---- + +=== Scaling Analysis + +==== Performance Scaling Patterns + +[source,kotlin] +---- +fun analyzeScalingBehavior( + sizeResults: Map +): ScalingAnalysis { + val sizes = sizeResults.keys.sorted() + val throughputs = sizes.map { sizeResults[it]!!.throughput } + + // Linear scaling: O(n) + val linearFit = performLinearRegression(sizes.map { it.toDouble() }, throughputs) + + // Quadratic scaling: O(n²) + val quadraticFit = performQuadraticRegression( + sizes.map { it.toDouble() }, + throughputs + ) + + return ScalingAnalysis( + linearRSquared = linearFit.rSquared, + quadraticRSquared = quadraticFit.rSquared, + bestFitModel = if (quadraticFit.rSquared > linearFit.rSquared) "Quadratic" else "Linear", + scalingEfficiency = calculateScalingEfficiency(sizes, throughputs) + ) +} +---- + +== Bottleneck Detection + +=== Resource Constraint Analysis + +==== Memory Bandwidth Bottlenecks + +[source,kotlin] +---- +fun analyzeMemoryBottleneck( + operationSize: Int, + executionTime: Double, + dataType: String +): MemoryAnalysis { + val bytesPerElement = when(dataType) { + "Float32" -> 4 + "Float64" -> 8 + else -> 4 + } + + val totalBytes = operationSize * bytesPerElement + val bandwidth = totalBytes / (executionTime / 1_000_000.0) // bytes/sec + val theoreticalBandwidth = getTheoreticalMemoryBandwidth() + + return MemoryAnalysis( + achievedBandwidth = bandwidth, + theoreticalBandwidth = theoreticalBandwidth, + efficiency = bandwidth / theoreticalBandwidth, + isMemoryBound = efficiency > 0.8 + ) +} +---- + +==== Computational Bottlenecks + +[source,kotlin] +---- +fun analyzeComputationalBottleneck( + flopsCount: Long, + executionTime: Double +): ComputeAnalysis { + val achievedFlops = flopsCount / (executionTime / 1_000_000.0) + val theoreticalFlops = getTheoreticalComputeCapacity() + + return ComputeAnalysis( + achievedFlops = achievedFlops, + theoreticalFlops = theoreticalFlops, + computeEfficiency = achievedFlops / theoreticalFlops, + isComputeBound = achievedFlops / theoreticalFlops > 0.7 + ) +} +---- + +=== Performance Profiling Integration + +==== Hotspot Analysis + +[source,kotlin] +---- +fun analyzePerformanceProfile( + profileData: ProfileData, + benchmarkResult: BenchmarkResult +): HotspotAnalysis { + val hotspots = profileData.methods + .sortedByDescending { it.totalTime } + .take(10) + + val totalProfileTime = profileData.methods.sumOf { it.totalTime } + + return HotspotAnalysis( + topHotspots = hotspots.map { method -> + HotspotInfo( + methodName = method.name, + totalTime = method.totalTime, + percentage = method.totalTime / totalProfileTime * 100, + callCount = method.callCount, + averageTime = method.totalTime / method.callCount + ) + }, + profileCorrelation = correlateWithBenchmark(profileData, benchmarkResult) + ) +} +---- + +== Trend Analysis + +=== Time Series Analysis + +==== Performance Trend Detection + +[source,kotlin] +---- +fun analyzeTrends( + historicalData: List +): TrendAnalysis { + val timestamps = historicalData.map { it.timestamp.toDouble() } + val throughputs = historicalData.map { it.averageThroughput } + + val linearTrend = performLinearRegression(timestamps, throughputs) + val seasonality = detectSeasonality(historicalData) + + return TrendAnalysis( + trend = when { + linearTrend.slope > 0.01 -> "Improving" + linearTrend.slope < -0.01 -> "Degrading" + else -> "Stable" + }, + trendStrength = abs(linearTrend.slope), + seasonalityDetected = seasonality.isPresent, + forecastNextPeriod = linearTrend.predict(getNextTimestamp()) + ) +} +---- + +=== Regression Detection + +==== Automated Regression Analysis + +[source,kotlin] +---- +fun detectPerformanceRegression( + baseline: BenchmarkResult, + current: BenchmarkResult, + threshold: Double = 0.05 +): RegressionResult { + val throughputChange = (current.throughput - baseline.throughput) / baseline.throughput + val latencyChange = (current.executionTime.mean - baseline.executionTime.mean) / baseline.executionTime.mean + + val isRegression = throughputChange < -threshold || latencyChange > threshold + + return RegressionResult( + isRegression = isRegression, + throughputChange = throughputChange, + latencyChange = latencyChange, + severity = when { + abs(throughputChange) > 0.20 -> RegressionSeverity.CRITICAL + abs(throughputChange) > 0.10 -> RegressionSeverity.MAJOR + abs(throughputChange) > 0.05 -> RegressionSeverity.MINOR + else -> RegressionSeverity.NONE + } + ) +} +---- + +== Optimization Insights + +=== Performance Recommendations + +==== Algorithmic Optimization + +[source,kotlin] +---- +fun generateOptimizationRecommendations( + analysis: ComprehensiveAnalysis +): List { + val recommendations = mutableListOf() + + // Memory optimization + if (analysis.memoryAnalysis.efficiency < 0.5) { + recommendations.add( + OptimizationRecommendation( + type = "Memory Layout", + priority = "High", + description = "Poor memory bandwidth utilization detected", + suggestions = listOf( + "Consider memory layout optimization", + "Implement cache-friendly access patterns", + "Use memory pooling for frequent allocations" + ) + ) + ) + } + + // Computational optimization + if (analysis.computeAnalysis.computeEfficiency < 0.6) { + recommendations.add( + OptimizationRecommendation( + type = "Computational", + priority = "Medium", + description = "Underutilized computational resources", + suggestions = listOf( + "Implement SIMD optimizations", + "Consider GPU acceleration", + "Optimize algorithmic complexity" + ) + ) + ) + } + + return recommendations +} +---- + +This comprehensive guide provides the analytical foundation for understanding and optimizing tensor operation performance in SKaiNET. \ No newline at end of file diff --git a/docs/modules/performance/pages/performance-testing.adoc b/docs/modules/performance/pages/performance-testing.adoc new file mode 100644 index 000000000..e947ef00e --- /dev/null +++ b/docs/modules/performance/pages/performance-testing.adoc @@ -0,0 +1,647 @@ += Performance Testing Guide +:toc: +:toclevels: 3 +:sectanchors: +:sectlinks: + +This guide provides comprehensive strategies and methodologies for performance testing in SKaiNET, covering test design, execution, analysis, and continuous performance monitoring. + +== Overview + +Performance testing ensures that tensor operations meet performance requirements across different backends, platforms, and workloads. This guide covers: + +* **Test Strategy**: Systematic approach to performance testing +* **Test Types**: Load testing, stress testing, scalability testing +* **Test Design**: Creating effective performance test suites +* **Execution**: Running tests in controlled environments +* **Analysis**: Interpreting results and identifying bottlenecks +* **Automation**: Integrating performance tests into CI/CD pipelines + +== Performance Testing Strategy + +=== Testing Objectives + +**Baseline Establishment**:: +Establish performance baselines for all critical tensor operations. + +**Regression Detection**:: +Identify performance degradations in new code changes. + +**Scalability Assessment**:: +Evaluate performance characteristics across different tensor sizes and workloads. + +**Backend Comparison**:: +Compare performance between different compute backends (CPU, GPU, etc.). + +**Resource Utilization**:: +Monitor memory usage, CPU utilization, and other system resources. + +=== Test Categories + +==== Unit Performance Tests + +Test individual tensor operations in isolation: + +[source,kotlin] +---- +@Test +fun testMatrixMultiplicationPerformance() { + val runner = BenchmarkRunner() + val matrixA = createRandomMatrix(512, 512) + val matrixB = createRandomMatrix(512, 512) + + val result = runner.benchmark( + name = "MatMul 512x512", + warmupRuns = 10, + measurementRuns = 100 + ) { + backend.matmul(matrixA, matrixB) + } + + // Assert performance requirements + assertTrue(result.throughput >= 100.0) // ops/sec + assertTrue(result.executionTime.mean < 50_000) // 50ms max +} +---- + +==== Integration Performance Tests + +Test complete workflows and operation sequences: + +[source,kotlin] +---- +@Test +fun testNeuralNetworkForwardPass() { + val network = createTestNetwork() + val input = createTestInput(batchSize = 32) + + val result = runner.benchmark( + name = "Forward Pass (32 batch)", + measurementRuns = 50 + ) { + network.forward(input) + } + + // Verify acceptable latency for inference + assertTrue(result.executionTime.mean < 100_000) // 100ms max +} +---- + +==== Scalability Tests + +Evaluate performance across different scales: + +[source,kotlin] +---- +@Test +fun testMatrixMultiplicationScaling() { + val sizes = listOf(64, 128, 256, 512, 1024, 2048) + val results = mutableMapOf() + + sizes.forEach { size -> + val matrixA = createRandomMatrix(size, size) + val matrixB = createRandomMatrix(size, size) + + results[size] = runner.benchmark( + name = "MatMul ${size}x${size}", + measurementRuns = 20 + ) { + backend.matmul(matrixA, matrixB) + } + } + + // Analyze scaling characteristics + analyzeScalingBehavior(results) +} +---- + +== Test Design Principles + +=== Realistic Workloads + +Design tests that reflect real-world usage patterns: + +**Typical Tensor Sizes**:: +Use tensor dimensions common in machine learning applications. + +**Operation Sequences**:: +Test realistic sequences of operations, not just isolated calls. + +**Data Patterns**:: +Use representative data distributions and sparsity patterns. + +**Batch Processing**:: +Include batch processing scenarios with various batch sizes. + +=== Controlled Environment + +Ensure consistent and reproducible test conditions: + +**System Isolation**:: +Run tests on dedicated hardware or in controlled environments. + +**Resource Monitoring**:: +Monitor CPU, memory, and I/O usage during tests. + +**Multiple Runs**:: +Execute tests multiple times to account for variability. + +**Environmental Factors**:: +Control temperature, power states, and background processes. + +=== Statistical Significance + +Design tests for statistically meaningful results: + +**Sample Sizes**:: +Use adequate sample sizes for statistical significance. + +**Confidence Intervals**:: +Calculate and report confidence intervals for measurements. + +**Outlier Handling**:: +Identify and appropriately handle performance outliers. + +**Variance Analysis**:: +Analyze variance to understand measurement reliability. + +== Test Implementation Patterns + +=== Parameterized Performance Tests + +Create flexible tests that cover multiple scenarios: + +[source,kotlin] +---- +class MatrixOperationPerformanceTest { + + @ParameterizedTest + @ValueSource(ints = [64, 128, 256, 512, 1024]) + fun testMatrixMultiplication(size: Int) { + val matrixA = createRandomMatrix(size, size) + val matrixB = createRandomMatrix(size, size) + + val result = benchmarkRunner.benchmark( + name = "MatMul ${size}x${size}", + measurementRuns = 50 + ) { + backend.matmul(matrixA, matrixB) + } + + // Size-specific performance assertions + val expectedThroughput = calculateExpectedThroughput(size) + assertTrue(result.throughput >= expectedThroughput) + + logResult(size, result) + } +} +---- + +=== Backend Comparison Tests + +Compare performance across different backends: + +[source,kotlin] +---- +@Test +fun testBackendComparison() { + val backends = listOf(cpuBackend, gpuBackend, simdBackend) + val testMatrix = createRandomMatrix(512, 512) + val results = mutableMapOf() + + backends.forEach { backend -> + results[backend.name] = benchmarkRunner.benchmark( + name = "${backend.name} MatMul", + measurementRuns = 100 + ) { + backend.matmul(testMatrix, testMatrix) + } + } + + // Generate comparison report + val comparison = generateBackendComparison(results) + println(comparison.prettyPrint()) + + // Assert relative performance expectations + assertTrue(results["GPU"]!!.throughput > results["CPU"]!!.throughput) +} +---- + +=== Memory Usage Tests + +Monitor memory consumption during operations: + +[source,kotlin] +---- +@Test +fun testMemoryUsageScaling() { + val sizes = listOf(100, 200, 500, 1000) + + sizes.forEach { size -> + val beforeMemory = getMemoryUsage() + + val matrices = List(10) { createRandomMatrix(size, size) } + val results = matrices.map { matrix -> + backend.matmul(matrix, matrix) + } + + val afterMemory = getMemoryUsage() + val memoryIncrease = afterMemory - beforeMemory + + // Assert memory usage is reasonable + val expectedMemory = calculateExpectedMemory(size, 10) + assertTrue(memoryIncrease <= expectedMemory * 1.2) // 20% tolerance + + // Clean up + results.forEach { it.dispose() } + } +} +---- + +== Test Execution Best Practices + +=== Environment Preparation + +**System Configuration**:: +- Use dedicated hardware for performance testing +- Disable unnecessary background services +- Set consistent CPU governor and power settings +- Ensure adequate cooling and thermal stability + +**JVM Settings**:: +- Use production JVM flags +- Set appropriate heap sizes +- Enable performance-oriented garbage collectors +- Disable debugging and profiling agents + +**Test Isolation**:: +- Run tests in separate processes when possible +- Reset system state between test suites +- Clear caches and buffers between iterations + +=== Execution Strategies + +==== Warm-up Protocols + +Implement proper warm-up to ensure stable measurements: + +[source,kotlin] +---- +fun executeWithWarmup(operation: () -> Unit): BenchmarkResult { + // JIT warm-up + repeat(100) { operation() } + + // Cache warm-up + repeat(20) { operation() } + + // Measurement phase + return benchmarkRunner.benchmark( + name = "Warmed Operation", + warmupRuns = 0, // Already warmed up + measurementRuns = 200 + ) { operation() } +} +---- + +==== Batch Execution + +Execute related tests in batches for efficiency: + +[source,kotlin] +---- +@TestMethodOrder(OrderAnnotation::class) +class BatchedPerformanceTests { + + companion object { + private val results = mutableMapOf() + + @AfterAll + @JvmStatic + fun generateReport() { + val report = BenchmarkReport( + backendName = "Test Backend", + results = results + ) + saveReport(report, "performance-results-${System.currentTimeMillis()}.json") + } + } + + @Test + @Order(1) + fun testSmallMatrices() { + results["small"] = benchmarkSmallMatrices() + } + + @Test + @Order(2) + fun testMediumMatrices() { + results["medium"] = benchmarkMediumMatrices() + } + + @Test + @Order(3) + fun testLargeMatrices() { + results["large"] = benchmarkLargeMatrices() + } +} +---- + +== Result Analysis and Interpretation + +=== Performance Metrics + +==== Primary Metrics + +**Throughput**:: +Operations per second, critical for batch processing scenarios. + +**Latency**:: +Response time, important for real-time applications. + +**Scalability**:: +How performance changes with increased load or data size. + +**Resource Efficiency**:: +Performance per unit of resource consumption. + +==== Secondary Metrics + +**Memory Usage**:: +Peak and sustained memory consumption patterns. + +**CPU Utilization**:: +Processor usage efficiency and parallelization effectiveness. + +**Cache Performance**:: +Cache hit rates and memory access patterns. + +**Energy Consumption**:: +Power efficiency, especially important for mobile/edge deployments. + +=== Statistical Analysis + +==== Descriptive Statistics + +[source,kotlin] +---- +fun analyzeResults(results: List) { + results.forEach { result -> + println("${result.name}:") + println(" Mean: ${result.executionTime.mean}μs") + println(" Std Dev: ${result.executionTime.standardDeviation}μs") + println(" 95th %ile: ${result.executionTime.percentile95}μs") + println(" Coefficient of Variation: ${ + result.executionTime.standardDeviation / result.executionTime.mean + }") + } +} +---- + +==== Regression Analysis + +Detect performance regressions: + +[source,kotlin] +---- +fun detectRegression( + baselineResults: List, + currentResults: List +): RegressionReport { + val regressions = mutableListOf() + + baselineResults.zip(currentResults).forEach { (baseline, current) -> + val performanceChange = (current.throughput - baseline.throughput) / baseline.throughput + + if (performanceChange < -0.05) { // 5% degradation threshold + regressions.add( + PerformanceRegression( + operationName = current.name, + baselineThroughput = baseline.throughput, + currentThroughput = current.throughput, + degradationPercent = performanceChange * 100 + ) + ) + } + } + + return RegressionReport(regressions) +} +---- + +=== Benchmarking Anti-patterns + +==== Common Pitfalls + +**Insufficient Warm-up**:: +Not allowing adequate time for JIT compilation and cache warming. + +**Microbenchmark Errors**:: +Testing operations that are too small or get optimized away. + +**Environmental Noise**:: +Running benchmarks on systems with variable background load. + +**Statistical Errors**:: +Using insufficient sample sizes or ignoring statistical significance. + +**Measurement Bias**:: +Including setup/teardown time in performance measurements. + +== Continuous Performance Monitoring + +=== CI/CD Integration + +==== Automated Performance Tests + +[source,gradle] +---- +// gradle/performance-test.gradle +task performanceTest(type: Test) { + useJUnitPlatform { + includeTags 'performance' + } + + systemProperty 'performance.baseline.file', 'performance-baseline.json' + systemProperty 'performance.results.dir', 'build/performance-results' + + // Ensure consistent environment + jvmArgs '-Xms2g', '-Xmx4g', '-XX:+UseG1GC' + + // Generate reports + finalizedBy 'generatePerformanceReport' +} +---- + +==== Performance Gates + +Implement performance gates in CI pipelines: + +[source,kotlin] +---- +@Test +@Tag("performance-gate") +fun criticalPathPerformanceGate() { + val result = benchmarkCriticalPath() + + // Hard limits for CI pipeline + assertTrue( + result.throughput >= MINIMUM_THROUGHPUT, + "Performance below acceptable threshold: ${result.throughput} < $MINIMUM_THROUGHPUT" + ) + + assertTrue( + result.executionTime.mean <= MAXIMUM_LATENCY, + "Latency above acceptable threshold: ${result.executionTime.mean} > $MAXIMUM_LATENCY" + ) +} +---- + +=== Performance Tracking + +==== Historical Tracking + +Maintain performance history for trend analysis: + +[source,kotlin] +---- +data class PerformanceDataPoint( + val timestamp: Long, + val gitCommit: String, + val buildNumber: Int, + val results: Map +) + +class PerformanceTracker { + fun recordResults(results: Map) { + val dataPoint = PerformanceDataPoint( + timestamp = System.currentTimeMillis(), + gitCommit = getCurrentGitCommit(), + buildNumber = getBuildNumber(), + results = results + ) + + persistDataPoint(dataPoint) + analyzePerformanceTrends(dataPoint) + } +} +---- + +==== Alerting and Reporting + +Set up automated alerting for performance issues: + +[source,kotlin] +---- +fun checkPerformanceThresholds(results: Map) { + val alerts = mutableListOf() + + results.forEach { (operation, result) -> + val baseline = getBaseline(operation) + val degradation = calculateDegradation(baseline, result) + + when { + degradation > 0.20 -> alerts.add( + PerformanceAlert.CRITICAL(operation, degradation) + ) + degradation > 0.10 -> alerts.add( + PerformanceAlert.WARNING(operation, degradation) + ) + } + } + + if (alerts.isNotEmpty()) { + sendAlerts(alerts) + updateDashboard(alerts) + } +} +---- + +== Platform-Specific Testing + +=== JVM Performance Testing + +**JIT Compilation Effects**:: +Account for Just-In-Time compilation warm-up periods. + +**Garbage Collection Impact**:: +Monitor and minimize GC effects on measurements. + +**JVM Flags**:: +Use production-appropriate JVM settings for accurate results. + +=== Native Performance Testing + +**Compilation Optimization**:: +Test with release builds and appropriate optimization flags. + +**Memory Management**:: +Monitor manual memory management performance. + +**Platform Variations**:: +Account for differences between target native platforms. + +=== JavaScript Performance Testing + +**Engine Differences**:: +Test across different JavaScript engines (V8, SpiderMonkey, etc.). + +**JIT Characteristics**:: +Understand JavaScript JIT compilation patterns. + +**Browser Environment**:: +Consider browser-specific performance characteristics. + +== Troubleshooting Performance Issues + +=== Common Performance Problems + +**Memory Leaks**:: +Use memory profiling tools to identify and fix leaks. + +**Inefficient Algorithms**:: +Profile code to identify algorithmic bottlenecks. + +**Cache Misses**:: +Analyze memory access patterns and optimize data layout. + +**Synchronization Overhead**:: +Identify and minimize unnecessary synchronization. + +=== Profiling Integration + +[source,kotlin] +---- +@Test +fun profiledPerformanceTest() { + val profiler = createProfiler() + + profiler.start() + val result = benchmarkRunner.benchmark( + name = "Profiled Operation", + measurementRuns = 100 + ) { + expensiveOperation() + } + profiler.stop() + + val profile = profiler.getResults() + analyzeHotSpots(profile) + generateProfileReport(result, profile) +} +---- + +=== Performance Debugging + +**Systematic Approach**:: +Use binary search to isolate performance bottlenecks. + +**Micro-benchmarks**:: +Create focused tests for suspected problem areas. + +**Comparative Analysis**:: +Compare performance across different implementations or backends. + +**Resource Monitoring**:: +Monitor system resources during performance issues. + +This comprehensive guide provides the foundation for effective performance testing in SKaiNET, ensuring that performance requirements are met and maintained throughout the development lifecycle. \ No newline at end of file From 34768ac4bc1f0433fadfafd9cf208cda9f6db54a Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 13:40:06 +0200 Subject: [PATCH 15/16] Add github action for build. --- .github/worklfows/build.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/worklfows/build.yml diff --git a/.github/worklfows/build.yml b/.github/worklfows/build.yml new file mode 100644 index 000000000..d9a11b8c9 --- /dev/null +++ b/.github/worklfows/build.yml @@ -0,0 +1,32 @@ +name: Build Feature Branch Apk + +on: [ push, pull_request ] + +jobs: + build-job: + runs-on: ubuntu-latest # enables hardware acceleration in the virtual machine + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 17 + + + - name: Print git commit variables + run: | + echo "TAG: $CURRENT_TAG" + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Build + run: ./gradlew --stacktrace clean assemble \ No newline at end of file From ccf54091f0ac12db560cb3cec0bcf889cc27dc74 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 15 Sep 2025 13:41:24 +0200 Subject: [PATCH 16/16] Add github action for build. --- .github/worklfows/build.yml | 2 +- .../src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/worklfows/build.yml b/.github/worklfows/build.yml index d9a11b8c9..b08583179 100644 --- a/.github/worklfows/build.yml +++ b/.github/worklfows/build.yml @@ -29,4 +29,4 @@ jobs: run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - name: Build - run: ./gradlew --stacktrace clean assemble \ No newline at end of file + run: ./gradlew --stacktrace clean assemble allTests \ No newline at end of file diff --git a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt index f1bff303c..7623fe630 100644 --- a/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt +++ b/skainet-core/skainet-tensors/src/commonTest/kotlin/sk/ainet/core/tensor/PrintingTest.kt @@ -1,7 +1,6 @@ package sk.ainet.core.tensor import sk.ai.net.core.tensor.Shape -import sk.ai.net.core.tensor.backend.* import sk.ainet.core.tensor.backend.CpuTensorFP32 import sk.ainet.core.tensor.backend.print import sk.ainet.core.tensor.backend.printMatrix