-
Notifications
You must be signed in to change notification settings - Fork 381
Expand file tree
/
Copy pathMetalContext.swift
More file actions
247 lines (224 loc) · 9.19 KB
/
Copy pathMetalContext.swift
File metadata and controls
247 lines (224 loc) · 9.19 KB
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import Foundation
import Metal
enum MetalError: Error, CustomStringConvertible {
case noDevice
case noQueue
case missingShaderResource(String)
case missingFunction(String)
case libraryCompileFailed(String)
public var description: String {
switch self {
case .noDevice: return "No Metal device"
case .noQueue: return "Failed to create Metal command queue"
case .missingShaderResource(let n): return "Shader resource missing: \(n)"
case .missingFunction(let n): return "Metal function missing in library: \(n)"
case .libraryCompileFailed(let s):return "Metal library compile failed: \(s)"
}
}
}
func checkCommandBufferError(_ error: (any Error)?) throws {
if let error {
throw error
}
}
public struct MetalFunctionConstant: Hashable, Sendable {
public enum Value: Hashable, Sendable {
case bool(Bool)
case uint32(UInt32)
case float(Float)
}
public let index: Int
public let value: Value
public init(index: Int, value: Value) {
self.index = index
self.value = value
}
}
/// Single owner of the `MTLDevice`, queue, and the runtime-compiled shader library.
/// On Mac and iOS we ship `.metal` source files as bundle resources and compile
/// them into one combined `MTLLibrary` at startup. This keeps the dev loop
/// fast — edit a shader, rebuild the Swift target, no Xcode metallib step.
/// `@unchecked Sendable`: device/queue/library are immutable and Metal objects
/// are thread-safe for encoding; the pipeline cache is the only mutable state
/// and is lock-guarded.
public final class MetalContext: @unchecked Sendable {
public let device: MTLDevice
public let queue: MTLCommandQueue
public let library: MTLLibrary
private struct PipelineCacheKey: Hashable {
var name: String
var constants: [MetalFunctionConstant]
var maxTotalThreadsPerThreadgroup: Int?
}
private var pipelineCache: [PipelineCacheKey: MTLComputePipelineState] = [:]
private let pipelineCacheLock = NSLock()
public init() throws {
guard let dev = MTLCreateSystemDefaultDevice() else { throw MetalError.noDevice }
guard let q = dev.makeCommandQueue() else { throw MetalError.noQueue }
self.device = dev
self.queue = q
self.library = try Self.compileShaderLibrary(device: dev)
}
/// Production shader modules compiled into the shared runtime library.
private static let shaderModules: [String] = [
"dequant_int4",
"dequant_int8",
"rmsnorm",
"rope",
"attention",
"moe",
"logit",
"utility",
"fused",
"prefill",
"vision",
]
/// Bundle locations for runtime shader modules.
private static let shaderSubdirectories: [String: String] = [
"attention": "Metal/Attention",
"dequant_int4": "Metal/Quant",
"dequant_int8": "Metal/Quant",
"fused": "Metal/Fusions",
"logit": "Metal/Sampling",
"moe": "Metal/MoE",
"prefill": "Metal/Prefill",
"rmsnorm": "Metal/Primitives",
"rope": "Metal/Primitives",
"tensorops": "Metal/TensorCore",
"utility": "Metal/Primitives",
"vision": "Metal/Vision",
"vision_register_gemm": "Metal/Vision",
"vision_resize": "Metal/Vision",
]
private static func shaderURL(module: String) -> URL? {
guard let subdirectory = shaderSubdirectories[module] else { return nil }
return Bundle.module.url(forResource: module, withExtension: "metal",
subdirectory: subdirectory)
}
private static func compileShaderLibrary(device: MTLDevice) throws -> MTLLibrary {
var combined = ""
for name in shaderModules {
guard let url = shaderURL(module: name) else {
throw MetalError.missingShaderResource(name)
}
let src = try String(contentsOf: url, encoding: .utf8)
combined += "\n// ==== \(name).metal ====\n" + src + "\n"
}
do {
let opts = MTLCompileOptions()
// The MPP prefill path requires MSL 4.0 tensor operations.
opts.languageVersion = .version4_0
return try device.makeLibrary(source: combined, options: opts)
} catch {
throw MetalError.libraryCompileFailed("\(error)")
}
}
/// Compile one shader module into its own library, leaving the shared
/// runtime library untouched.
///
/// Cached per device, module, math mode and source variant: libraries are
/// immutable, and
/// one `VisionRuntime` init otherwise compiles the identical tensorops
/// source three times (linear, attention, projector) on every load.
public static func privateLibrary(device: MTLDevice, module: String,
mathMode: MTLMathMode? = nil,
includeVisionTensorOps: Bool = false) throws
-> MTLLibrary {
let key = "\(ObjectIdentifier(device).hashValue)#\(module)"
+ "#\(mathMode?.rawValue ?? -1)#\(includeVisionTensorOps)"
privateLibraryLock.lock()
defer { privateLibraryLock.unlock() }
if let cached = privateLibraryCache[key] {
return cached
}
guard let url = shaderURL(module: module) else {
throw MetalError.missingShaderResource(module)
}
let src = try String(contentsOf: url, encoding: .utf8)
let opts = MTLCompileOptions()
opts.languageVersion = .version4_0
if let mathMode {
opts.mathMode = mathMode
}
if includeVisionTensorOps {
opts.preprocessorMacros = [
"TURBO_FIELDFARE_VISION_TENSOROPS": NSNumber(value: true)
]
}
do {
let library = try device.makeLibrary(source: src, options: opts)
privateLibraryCache[key] = library
return library
} catch {
throw MetalError.libraryCompileFailed("\(error)")
}
}
private static let privateLibraryLock = NSLock()
private static nonisolated(unsafe) var privateLibraryCache: [String: MTLLibrary] = [:]
public func pipeline(_ name: String) throws -> MTLComputePipelineState {
try pipeline(name, constants: [])
}
public func pipeline(_ name: String,
constants: [MetalFunctionConstant]) throws -> MTLComputePipelineState {
try pipeline(name, constants: constants, maxTotalThreadsPerThreadgroup: nil)
}
public func pipeline(_ name: String,
constants: [MetalFunctionConstant],
maxTotalThreadsPerThreadgroup hint: Int?) throws -> MTLComputePipelineState {
if let hint {
precondition(hint > 0, "maxTotalThreadsPerThreadgroup must be positive")
}
let sortedConstants = constants.sorted {
if $0.index != $1.index { return $0.index < $1.index }
return Self.constantSortKey($0.value) < Self.constantSortKey($1.value)
}
let key = PipelineCacheKey(name: name,
constants: sortedConstants,
maxTotalThreadsPerThreadgroup: hint)
pipelineCacheLock.lock()
let cached = pipelineCache[key]
pipelineCacheLock.unlock()
if let cached { return cached }
guard library.functionNames.contains(name) else {
throw MetalError.missingFunction(name)
}
let values = MTLFunctionConstantValues()
for constant in sortedConstants {
switch constant.value {
case .bool(let value):
var v = value
values.setConstantValue(&v, type: .bool, index: constant.index)
case .uint32(let value):
var v = value
values.setConstantValue(&v, type: .uint, index: constant.index)
case .float(let value):
var v = value
values.setConstantValue(&v, type: .float, index: constant.index)
}
}
let fn = try library.makeFunction(name: name, constantValues: values)
let p: MTLComputePipelineState
if let hint {
let descriptor = MTLComputePipelineDescriptor()
descriptor.computeFunction = fn
descriptor.maxTotalThreadsPerThreadgroup = hint
var reflection: MTLAutoreleasedComputePipelineReflection?
p = try device.makeComputePipelineState(descriptor: descriptor,
options: [],
reflection: &reflection)
} else {
p = try device.makeComputePipelineState(function: fn)
}
pipelineCacheLock.lock()
pipelineCache[key] = p
pipelineCacheLock.unlock()
return p
}
private static func constantSortKey(_ value: MetalFunctionConstant.Value) -> String {
switch value {
case .bool(let v): return "b:\(v ? 1 : 0)"
case .uint32(let v): return "u:\(v)"
case .float(let v): return "f:\(v.bitPattern)"
}
}
}