Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@

### Fixed

- **Memory-copy diagnostics attribute copies to their source.** `MemoryTracker.recordCopy`
discarded the `sourceName` every instrumented call site passes; reports now carry a
per-source breakdown (`copiesBySource: Map<String, CopySourceStat>`, included in the
report's text form, sorted by volume). `ActiveMemoryTracker.current` is now `@Volatile`
with an honest thread-safety contract in its docs; making the tracker per-execution-context
instead of a process-wide hook is part of the SKEEP-003 storage-model discussion. (#931)
- **`TensorStorageFactory` ownership labels are now truthful.** `borrowFloatArray` re-encoded
the floats into a private byte copy and labeled it `Borrowed` despite its "(zero-copy)" doc —
it is now deprecated (delegating to `fromFloatArray`) and honestly returns `Owned`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package sk.ainet.lang.tensor.storage

import kotlin.concurrent.Volatile

/**
* Global hook for the active [MemoryTracker].
*
* Set [current] to a tracker instance to automatically capture copy events
* from instrumented copy paths (e.g. CopyMaterializationStrategy,
* DenseTensorDataFactory.from*Array). Set to `null` to disable tracking.
*
* Thread-safety note: on JVM this should ideally be a ThreadLocal.
* For now, a simple global works for single-threaded inference.
* Thread-safety: [current] is `@Volatile`, so installing or clearing a
* tracker is immediately visible to other threads. [MemoryTracker] itself
* is not synchronized — concurrent loads/inference sessions that need
* isolated attribution should each install their own tracker around their
* critical section, or serialize access. Making the tracker installable
* per execution context (instead of a process-wide hook) is part of the
* storage-model discussion in SKEEP-003.
*/
public object ActiveMemoryTracker {
@Volatile
public var current: MemoryTracker? = null

/** Record a copy event on the active tracker, if any. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,35 @@ public class MemoryTracker {
private val entries = mutableListOf<TrackedEntry>()
private var copyCount: Long = 0
private var copyBytes: Long = 0
private val copiesBySource = mutableMapOf<String, CopySourceStat>()

/** Record a tensor storage allocation. */
public fun record(name: String, storage: TensorStorage) {
entries.add(TrackedEntry(name, storage.memoryReport()))
}

/** Record an explicit copy event (for copy-tracing). */
/**
* Record an explicit copy event (for copy-tracing).
*
* [sourceName] is aggregated per source, so reports can attribute copy
* volume to the code path that produced it (e.g. which factory or
* materialization strategy) — previously the label was discarded.
*/
public fun recordCopy(sourceName: String, bytes: Long) {
copyCount++
copyBytes += bytes
val prev = copiesBySource[sourceName]
copiesBySource[sourceName] =
if (prev == null) CopySourceStat(count = 1, bytes = bytes)
else CopySourceStat(count = prev.count + 1, bytes = prev.bytes + bytes)
}

/** Reset all tracked entries. */
public fun clear() {
entries.clear()
copyCount = 0
copyBytes = 0
copiesBySource.clear()
}

/** Generate an aggregate memory report. */
Expand Down Expand Up @@ -69,11 +81,18 @@ public class MemoryTracker {
fileBackedCount = fileBackedCount,
copyCount = copyCount,
copyBytes = copyBytes,
entries = entries.toList()
entries = entries.toList(),
copiesBySource = copiesBySource.toMap()
)
}
}

/** Per-source copy statistics: how many copies a code path produced and their total volume. */
public data class CopySourceStat(
val count: Long,
val bytes: Long
)

public data class TrackedEntry(
val name: String,
val report: StorageMemoryReport
Expand All @@ -90,7 +109,8 @@ public data class AggregateMemoryReport(
val fileBackedCount: Int,
val copyCount: Long,
val copyBytes: Long,
val entries: List<TrackedEntry>
val entries: List<TrackedEntry>,
val copiesBySource: Map<String, CopySourceStat> = emptyMap()
) {
val overallCompressionRatio: Double
get() = if (totalPhysicalBytes > 0) totalLogicalBytes.toDouble() / totalPhysicalBytes else 1.0
Expand All @@ -103,6 +123,12 @@ public data class AggregateMemoryReport(
appendLine("File-backed: $fileBackedCount ($fileBackedBytes bytes)")
appendLine("Owned: $ownedCount, Borrowed: $borrowedCount, Aliased: $aliasedCount")
appendLine("Copies: $copyCount ($copyBytes bytes)")
if (copiesBySource.isNotEmpty()) {
appendLine("--- Copies by source ---")
for ((source, stat) in copiesBySource.entries.sortedByDescending { it.value.bytes }) {
appendLine(" $source: ${stat.count} (${stat.bytes} bytes)")
}
}
if (entries.isNotEmpty()) {
appendLine("--- Per-tensor ---")
for (e in entries) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,45 @@ class ActiveMemoryTrackerTest {
assertEquals(100L, report.copyBytes)
}

@Test
fun recordCopy_attributesPerSource() {
// The source label used to be discarded (#931) — every call site
// passes a meaningful one, and reports must break copies down by it.
val tracker = MemoryTracker()
ActiveMemoryTracker.current = tracker

ActiveMemoryTracker.recordCopy("factory", 100)
ActiveMemoryTracker.recordCopy("factory", 50)
ActiveMemoryTracker.recordCopy("materialize", 200)

val report = tracker.report()
assertEquals(3L, report.copyCount)
assertEquals(350L, report.copyBytes)
assertEquals(CopySourceStat(count = 2, bytes = 150), report.copiesBySource["factory"])
assertEquals(CopySourceStat(count = 1, bytes = 200), report.copiesBySource["materialize"])
}

@Test
fun clear_resetsPerSourceAttribution() {
val tracker = MemoryTracker()
tracker.recordCopy("a", 10)
tracker.clear()
tracker.recordCopy("b", 20)

val report = tracker.report()
assertEquals(1L, report.copyCount)
assertEquals(mapOf("b" to CopySourceStat(1, 20)), report.copiesBySource)
}

@Test
fun report_toString_includesPerSourceBreakdown() {
val tracker = MemoryTracker()
tracker.recordCopy("DenseTensorDataFactory.createFloatTensorData", 4096)
val text = tracker.report().toString()
kotlin.test.assertTrue("DenseTensorDataFactory.createFloatTensorData" in text, text)
kotlin.test.assertTrue("4096" in text, text)
}

@Test
fun recordCopy_withNullTracker_noOp() {
ActiveMemoryTracker.current = null
Expand Down
Loading