File size: 7,728 Bytes
743fd20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package com.iris3b

import java.io.File
import java.nio.file.Files
import java.security.MessageDigest

/**
 * The files of one run in the app's files dir, as the rows file names them (no Android API, so the
 * JVM check reads the very files the phone gets):
 *
 *  - rows file: `{"kind": "generic", "config": <config file>, "inputs": [{"file": ...}, ...]
 *    (every file the run needs; devicebench pushes them), "rows": [{"key", "prompt", "noise"}]}`;
 *  - config file: the residency policy (P1 / P2 / P4, with P4's `prefetch` rule), the dumps, the
 *    tokenizer and embedding files, the host tensor pack and its index, and per graph its file,
 *    accel, precision, signature, input names in feed order and output names with element counts;
 *  - tokenizer file: `{"files": {name: text}}`, written out to `tok/` for [PromptTokens], with
 *    `tok/embed_bf16.bin` a symbolic link to the embedding table.
 */
class RunFiles(val dir: File, rowsName: String) {

    class Row(val key: String, val prompt: String, val noise: String)

    class Tensor(val offset: Long, val count: Int, val shape: List<Int>)

    val rowsDoc: Map<String, Any?> = obj(Json.parse(File(dir, rowsName).readText()))
    val configName: String = rowsDoc.str("config")
    val config: Map<String, Any?> = obj(Json.parse(File(dir, configName).readText()))
    val rows: List<Row> = rowsDoc.list("rows").map {
        val r = obj(it)
        Row(r.str("key"), r.str("prompt"), (r["noise"] as? String) ?: "noise_pix")
    }
    val residency: Residency = Residency.valueOf(config.str("residency"))

    /**
     * Graph roles whose GPU program cache is serialized, as role prefixes: `"program_cache": true`
     * = every graph, a list (e.g. `["dit_"]`) = the roles starting with one of them, absent = none.
     */
    val programCacheRoles: List<String> = when (val pc = config["program_cache"]) {
        true -> listOf("")
        is List<*> -> pc.map { it as String }
        else -> emptyList()
    }
    val programCache: Boolean = programCacheRoles.isNotEmpty()
    val dumps: List<String> = (config["dumps"] as? List<*>)?.map { it as String } ?: emptyList()

    private val hostFile = File(dir, obj(config["host"]).str("file"))
    private val hostIndex: Map<String, Tensor> = obj(obj(config["host"])["tensors"]).mapValues { (_, v) ->
        val t = obj(v)
        Tensor(t.num("offset").toLong(), t.num("count").toInt(), (t["shape"] as List<*>).map { (it as Double).toInt() })
    }
    private val hostCache = HashMap<String, FloatArray>()

    /** Host tensors from the pack, each read once. */
    val host = HostTensors { name ->
        hostCache.getOrPut(name) {
            val t = requireNotNull(hostIndex[name]) { "host tensor $name is not in ${hostFile.name}" }
            HostOps.readFloats(hostFile, t.offset, t.count)
        }
    }

    fun hostShape(name: String): List<Int> = requireNotNull(hostIndex[name]) { "no host tensor $name" }.shape

    /** Every file the rows file declares, with a check that each is a regular file. */
    fun missingInputs(): List<String> =
        rowsDoc.list("inputs").map { obj(it).str("file") }.filter { !File(dir, it).isFile }

    /** The [EngineConfig] the config file describes. */
    fun engineConfig(): EngineConfig {
        val g = obj(config["graphs"])
        return EngineConfig(
            residency = residency,
            textEncoder = g.list("te").map { spec(it) },
            adapter = spec(g["adapter"]),
            dual = g.list("dual").map { spec(it) },
            dualModRows = g.list("dual_mod_rows").map { range(it) },
            single = g.list("single").map { spec(it) },
            singleModRows = g.list("single_mod_rows").map { range(it) },
            pixel = spec(g["pixel"]),
            dumps = dumps.toSet(),
            prefetch = prefetchRule(),
        )
    }

    /** P4's memory rule: `"prefetch": {"factor": x, "margin_bytes": n}`, each defaulting to [PrefetchRule]'s. */
    private fun prefetchRule(): PrefetchRule {
        val p = config["prefetch"] as? Map<*, *> ?: return PrefetchRule()
        val default = PrefetchRule()
        return PrefetchRule(
            factor = (p["factor"] as? Double) ?: default.factor,
            marginBytes = (p["margin_bytes"] as? Double)?.toLong() ?: default.marginBytes,
        )
    }

    /** Every graph file the config names, in pipeline order. */
    fun graphFiles(): List<String> {
        val c = engineConfig()
        return (c.textEncoder + c.adapter + c.dual + c.single + c.pixel).map { it.file }
    }

    /**
     * Writes the tokenizer files into `tok/` and links `tok/embed_bf16.bin` to the embedding
     * table; returns the sha256 of every file written.
     */
    fun materializeTokenizer(): Map<String, String> {
        val tok = File(dir, "tok")
        tok.mkdirs()
        val files = obj(obj(Json.parse(File(dir, config.str("tokenizer")).readText()))["files"])
        val sums = LinkedHashMap<String, String>()
        for ((name, text) in files) {
            val bytes = (text as String).toByteArray(Charsets.UTF_8)
            File(tok, name).writeBytes(bytes)
            sums[name] = sha256(bytes)
        }
        val link = File(tok, PromptTokens.EMBEDDING_FILE).toPath()
        Files.deleteIfExists(link)
        Files.createSymbolicLink(link, File(dir, config.str("embed")).absoluteFile.toPath())
        return sums
    }

    /** The output tensors of one row in out-file order: the image, then the dumps. */
    fun outputs(): List<Pair<String, Int>> {
        val out = arrayListOf("image" to HostOps.IMAGE_FLOATS)
        for (d in dumps) {
            when (d) {
                "te" -> out.add("te" to HostOps.TE_FLOATS)
                "y0" -> out.add("y0" to HostOps.WINDOW * HostOps.DIT_HIDDEN)
                "v" -> for (s in 0 until 4) out.add("v_s$s" to HostOps.PATCHES * HostOps.PIXELS * HostOps.CHANNELS)
                else -> throw IllegalArgumentException("unknown dump $d")
            }
        }
        return out
    }

    private fun spec(any: Any?): GraphSpec {
        val m = obj(any)
        val accel = (m["accel"] as? String) ?: "gpu"
        require(accel == "gpu" || accel == "cpu") { "${m["role"]}: accel $accel (gpu or cpu)" }
        val file = m.str("file")
        return GraphSpec(
            role = m.str("role"),
            file = file,
            precision = m.str("precision"),
            signature = m.str("signature"),
            inputs = m.list("inputs").map { it as String },
            outputs = m.list("outputs").map {
                val pair = it as List<*>
                (pair[0] as String) to (pair[1] as Double).toInt()
            },
            accel = accel,
            threads = ((m["threads"] ?: config["cpu_threads"]) as? Double)?.toInt() ?: 4,
            xnnpackCache = m["xnnpack_cache"] as? Boolean ?: false,
            bytes = File(dir, file).length(),
        )
    }

    private fun range(any: Any?): IntArray = (any as List<*>).map { (it as Double).toInt() }.toIntArray()

    companion object {
        @Suppress("UNCHECKED_CAST")
        fun obj(any: Any?): Map<String, Any?> = requireNotNull(any as? Map<String, Any?>) { "expected a JSON object" }

        fun Map<String, Any?>.str(key: String): String = requireNotNull(this[key] as? String) { "missing string $key" }

        fun Map<String, Any?>.num(key: String): Double = requireNotNull(this[key] as? Double) { "missing number $key" }

        fun Map<String, Any?>.list(key: String): List<Any?> = requireNotNull(this[key] as? List<*>) { "missing list $key" }

        fun sha256(bytes: ByteArray): String =
            MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") { "%02x".format(it) }
    }
}