Reza2kn commited on
Commit
d82e1f6
·
verified ·
1 Parent(s): 92c0d8d

Add native Swift CoreML runtime and assets

Browse files
Files changed (25) hide show
  1. README.md +42 -12
  2. coreml_runtime_manifest.json +4 -6
  3. native/SuryaCoreMLRuntime/Package.swift +25 -0
  4. native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/DecodeState.swift +77 -0
  5. native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/MLMultiArrayUtilities.swift +101 -0
  6. native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/QwenFixedImagePreprocessor.swift +128 -0
  7. native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/SuryaCoreMLRuntime.swift +152 -0
  8. native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/SuryaModelBundle.swift +35 -0
  9. native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/SuryaNativeArrays.swift +109 -0
  10. native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/SuryaNativeConstants.swift +83 -0
  11. native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/TokenDecoder.swift +65 -0
  12. native/SuryaCoreMLRuntime/Sources/SuryaCoreMLSmoke/main.swift +130 -0
  13. native/SuryaCoreMLRuntime/Tests/SuryaCoreMLRuntimeTests/SuryaCoreMLRuntimeTests.swift +28 -0
  14. native_assets/canary_pixel_values_fp16.bin +3 -0
  15. native_assets/decode_cos_fp16.bin +3 -0
  16. native_assets/decode_sin_fp16.bin +3 -0
  17. native_assets/id_to_token.json +0 -0
  18. native_assets/prefill_cos_fp16.bin +3 -0
  19. native_assets/prefill_sin_fp16.bin +3 -0
  20. native_assets/prefill_text_embeds_base_fp16.bin +3 -0
  21. native_assets/surya_native_constants.json +1222 -0
  22. native_assets/swift_pixel_values_fp16.bin +3 -0
  23. native_assets/token_embedding_fp16.bin +3 -0
  24. scripts/materialize_surya_native_assets.py +129 -0
  25. validation/native_swift_int8_smoke.json +8 -0
README.md CHANGED
@@ -22,7 +22,7 @@ tags:
22
 
23
  This repository contains an early CoreML runtime bundle derived from [`datalab-to/surya-ocr-2`](https://huggingface.co/datalab-to/surya-ocr-2) at source commit `3b3d4cdf88d6928b0acdc75181b13206ea67c4a3`.
24
 
25
- It is intended for native Apple OCR experiments and future iOS/macOS demo-app work. It is **not yet a drop-in app runtime**: the model packages pass canary runtime gates, but host-side glue is still required.
26
 
27
  ## Included packages
28
 
@@ -33,7 +33,21 @@ It is intended for native Apple OCR experiments and future iOS/macOS demo-app wo
33
  | `surya_prefill_fp16_seq300_cache512.mlpackage` | Language prefill, logits, and initial cache | CoreML FP16 compute | fixed prefill length `300`, cache length `512` |
34
  | `surya_decode_step_fp16_cache512.mlpackage` | One-token cached decode step | CoreML FP16 compute | one token at a time, cache length `512` |
35
 
36
- Processor/tokenizer assets are included under `processor/`. Validation JSONs are under `validation/`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  ## Current validation
39
 
@@ -56,22 +70,20 @@ Total $42.00
56
  | CoreML prefill -> CoreML decode | 9/9 tokens match native; text `<p>Invoice ` |
57
  | CoreML FP16 vision -> CoreML prefill -> CoreML decode | 9/9 tokens match native; text `<p>Invoice ` |
58
  | CoreML INT8 vision -> CoreML prefill -> CoreML decode | 9/9 tokens match native; text `<p>Invoice ` |
 
59
 
60
  The INT8 vision package has mean absolute diff `0.021211756393313408` vs the PyTorch vision tower on the canary.
61
 
62
  ## What still lives in host code
63
 
64
- This is a runtime component bundle, not a complete Swift package yet. The current host/application layer must handle:
65
 
66
- - tokenization
67
- - initial text token embedding lookup
68
- - image placeholder insertion
69
- - rotary position embedding generation
70
- - generated-token embedding lookup
71
- - full-attention KV cache insertion
72
- - stopping criteria and HTML/post-processing
73
 
74
- The included `scripts/export_surya_coreml_runtime.py` shows the exact Python reference glue used for the validation runs.
75
 
76
  ## Example: run the current validation harness
77
 
@@ -97,13 +109,31 @@ Expected canary output today:
97
  }
98
  ```
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  ## Important limitations
101
 
102
  - Fixed-shape canary export: the current packages are specialized to the traced 512x512 sample preprocessing path, with `pixel_values [1024,1536]`, prefill length `300`, and full-attention cache length `512`.
103
  - The language prefill/decode packages are FP16 CoreML, not INT8/4-bit yet.
104
  - Only the vision tower has an INT8 package in this release.
105
  - This has not yet passed full `allenai/olmOCR-bench`.
106
- - This is not a complete native OCR app. It is the validated CoreML model core we will build the app around.
107
 
108
  ## Provenance
109
 
 
22
 
23
  This repository contains an early CoreML runtime bundle derived from [`datalab-to/surya-ocr-2`](https://huggingface.co/datalab-to/surya-ocr-2) at source commit `3b3d4cdf88d6928b0acdc75181b13206ea67c4a3`.
24
 
25
+ It is intended for native Apple OCR experiments and future iOS/macOS demo-app work. This revision includes a SwiftPM runtime package that runs the fixed-shape OCR canary path without Python.
26
 
27
  ## Included packages
28
 
 
33
  | `surya_prefill_fp16_seq300_cache512.mlpackage` | Language prefill, logits, and initial cache | CoreML FP16 compute | fixed prefill length `300`, cache length `512` |
34
  | `surya_decode_step_fp16_cache512.mlpackage` | One-token cached decode step | CoreML FP16 compute | one token at a time, cache length `512` |
35
 
36
+ Processor/tokenizer assets are included under `processor/`. Native constants and embedding assets are under `native_assets/`. Validation JSONs are under `validation/`.
37
+
38
+ ## Native Swift package
39
+
40
+ The SwiftPM package lives in `native/SuryaCoreMLRuntime`.
41
+
42
+ ```swift
43
+ import SuryaCoreMLRuntime
44
+
45
+ let runtime = try SuryaCoreMLRuntime(modelDirectory: modelDirectoryURL)
46
+ let result = try runtime.generate(image: cgImage, maxNewTokens: 128, useInt8Vision: true)
47
+ print(result.text ?? "")
48
+ ```
49
+
50
+ The package currently handles fixed 512x512 Qwen image preprocessing, INT8/FP16 vision selection, fixed OCR prompt embeddings, image placeholder insertion, precomputed RoPE tables, generated-token embedding lookup, KV cache insertion, CoreML prefill/decode, and basic token decoding.
51
 
52
  ## Current validation
53
 
 
70
  | CoreML prefill -> CoreML decode | 9/9 tokens match native; text `<p>Invoice ` |
71
  | CoreML FP16 vision -> CoreML prefill -> CoreML decode | 9/9 tokens match native; text `<p>Invoice ` |
72
  | CoreML INT8 vision -> CoreML prefill -> CoreML decode | 9/9 tokens match native; text `<p>Invoice ` |
73
+ | Native Swift image -> CoreML INT8 vision -> CoreML prefill -> CoreML decode | 9/9 tokens match native; text `<p>Invoice ` |
74
 
75
  The INT8 vision package has mean absolute diff `0.021211756393313408` vs the PyTorch vision tower on the canary.
76
 
77
  ## What still lives in host code
78
 
79
+ The included Swift package is a fixed-shape native runtime for the current OCR path. The host app still owns:
80
 
81
+ - providing a `CGImage` or already-preprocessed `pixel_values`
82
+ - stopping criteria and max-token policy
83
+ - UI and post-processing of generated OCR HTML
84
+ - expanding beyond the current fixed 512x512 image / 300-token prefill shape
 
 
 
85
 
86
+ The included `scripts/export_surya_coreml_runtime.py` shows the Python reference glue used for validation. The included `native/SuryaCoreMLRuntime` package is the native implementation.
87
 
88
  ## Example: run the current validation harness
89
 
 
109
  }
110
  ```
111
 
112
+ ## Example: run the native Swift smoke
113
+
114
+ ```bash
115
+ cd native/SuryaCoreMLRuntime
116
+ swift run surya-coreml-smoke \
117
+ --model-dir ../.. \
118
+ --image /path/to/512x512-document.png \
119
+ --max-tokens 8 \
120
+ --vision int8
121
+ ```
122
+
123
+ Validated canary output:
124
+
125
+ ```text
126
+ 1039 2009 2046 2054 2047 2041 2035 2037 1979
127
+ <p>Invoice
128
+ ```
129
+
130
  ## Important limitations
131
 
132
  - Fixed-shape canary export: the current packages are specialized to the traced 512x512 sample preprocessing path, with `pixel_values [1024,1536]`, prefill length `300`, and full-attention cache length `512`.
133
  - The language prefill/decode packages are FP16 CoreML, not INT8/4-bit yet.
134
  - Only the vision tower has an INT8 package in this release.
135
  - This has not yet passed full `allenai/olmOCR-bench`.
136
+ - The Swift decoder is intentionally small and should be hardened against the full tokenizer before production use.
137
 
138
  ## Provenance
139
 
coreml_runtime_manifest.json CHANGED
@@ -28,14 +28,12 @@
28
  "combined_prefill_decode_tokens_match": "9/9",
29
  "vision_fp16_prefill_decode_tokens_match": "9/9",
30
  "vision_int8_prefill_decode_tokens_match": "9/9",
 
31
  "canary_text": "<p>Invoice "
32
  },
33
  "host_responsibilities": [
34
- "tokenization",
35
- "initial text token embedding lookup",
36
- "image placeholder insertion",
37
- "rotary position embedding generation",
38
- "generated-token embedding lookup",
39
- "full-attention KV cache insertion"
40
  ]
41
  }
 
28
  "combined_prefill_decode_tokens_match": "9/9",
29
  "vision_fp16_prefill_decode_tokens_match": "9/9",
30
  "vision_int8_prefill_decode_tokens_match": "9/9",
31
+ "native_swift_vision_int8_prefill_decode_tokens_match": "9/9",
32
  "canary_text": "<p>Invoice "
33
  },
34
  "host_responsibilities": [
35
+ "provide a CGImage or preprocessed pixel_values",
36
+ "choose stopping criteria",
37
+ "post-process generated OCR HTML"
 
 
 
38
  ]
39
  }
native/SuryaCoreMLRuntime/Package.swift ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // swift-tools-version: 6.0
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "SuryaCoreMLRuntime",
6
+ platforms: [
7
+ .iOS(.v17),
8
+ .macOS(.v14),
9
+ ],
10
+ products: [
11
+ .library(name: "SuryaCoreMLRuntime", targets: ["SuryaCoreMLRuntime"]),
12
+ .executable(name: "surya-coreml-smoke", targets: ["SuryaCoreMLSmoke"]),
13
+ ],
14
+ targets: [
15
+ .target(name: "SuryaCoreMLRuntime"),
16
+ .executableTarget(
17
+ name: "SuryaCoreMLSmoke",
18
+ dependencies: ["SuryaCoreMLRuntime"]
19
+ ),
20
+ .testTarget(
21
+ name: "SuryaCoreMLRuntimeTests",
22
+ dependencies: ["SuryaCoreMLRuntime"]
23
+ ),
24
+ ]
25
+ )
native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/DecodeState.swift ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import CoreML
2
+ import Foundation
3
+
4
+ final class DecodeState {
5
+ var fullKeys: [MLMultiArray]
6
+ var fullValues: [MLMultiArray]
7
+ var convStates: [MLMultiArray]
8
+ var recurrentStates: [MLMultiArray]
9
+
10
+ init(prefillOutput: MLFeatureProvider, promptLength: Int) throws {
11
+ self.fullKeys = try (0..<SuryaConstants.fullAttentionLayers).map {
12
+ try prefillOutput.requiredArray("full_key_\($0)")
13
+ }
14
+ self.fullValues = try (0..<SuryaConstants.fullAttentionLayers).map {
15
+ try prefillOutput.requiredArray("full_value_\($0)")
16
+ }
17
+ self.convStates = try (0..<SuryaConstants.linearAttentionLayers).map {
18
+ try prefillOutput.requiredArray("conv_state_\($0)")
19
+ }
20
+ self.recurrentStates = try (0..<SuryaConstants.linearAttentionLayers).map {
21
+ try prefillOutput.requiredArray("recurrent_state_\($0)")
22
+ }
23
+
24
+ guard fullKeys.count == SuryaConstants.fullAttentionLayers,
25
+ fullValues.count == SuryaConstants.fullAttentionLayers,
26
+ convStates.count == SuryaConstants.linearAttentionLayers,
27
+ recurrentStates.count == SuryaConstants.linearAttentionLayers else {
28
+ throw SuryaRuntimeError.invalidShape("Missing cache state outputs")
29
+ }
30
+
31
+ _ = promptLength
32
+ }
33
+
34
+ func apply(decodeOutput: MLFeatureProvider, cacheIndex: Int) throws {
35
+ for layer in 0..<SuryaConstants.fullAttentionLayers {
36
+ let newKey = try decodeOutput.requiredArray("new_full_key_\(layer)")
37
+ let newValue = try decodeOutput.requiredArray("new_full_value_\(layer)")
38
+ try insertFullAttentionSlice(source: newKey, destination: fullKeys[layer], cacheIndex: cacheIndex)
39
+ try insertFullAttentionSlice(source: newValue, destination: fullValues[layer], cacheIndex: cacheIndex)
40
+ }
41
+
42
+ self.convStates = try (0..<SuryaConstants.linearAttentionLayers).map {
43
+ try decodeOutput.requiredArray("new_conv_state_\($0)")
44
+ }
45
+ self.recurrentStates = try (0..<SuryaConstants.linearAttentionLayers).map {
46
+ try decodeOutput.requiredArray("new_recurrent_state_\($0)")
47
+ }
48
+ }
49
+
50
+ private func insertFullAttentionSlice(source: MLMultiArray, destination: MLMultiArray, cacheIndex: Int) throws {
51
+ let dstShape = destination.shape.map(\.intValue)
52
+ guard dstShape.count == 4 else {
53
+ throw SuryaRuntimeError.invalidShape("Expected full cache rank 4, got \(dstShape)")
54
+ }
55
+ let batch = dstShape[0]
56
+ let heads = dstShape[1]
57
+ let cacheLength = dstShape[2]
58
+ let dim = dstShape[3]
59
+ guard batch == 1, cacheIndex >= 0, cacheIndex < cacheLength else {
60
+ throw SuryaRuntimeError.invalidShape("Bad cache index \(cacheIndex) for shape \(dstShape)")
61
+ }
62
+
63
+ let sourceCount = heads * dim
64
+ guard source.count == sourceCount || source.count == batch * sourceCount else {
65
+ throw SuryaRuntimeError.invalidShape("New KV slice count \(source.count), expected \(sourceCount)")
66
+ }
67
+
68
+ for head in 0..<heads {
69
+ for d in 0..<dim {
70
+ let srcIndex = head * dim + d
71
+ let dstIndex = ((head * cacheLength) + cacheIndex) * dim + d
72
+ let value = try source.readFloat(at: srcIndex)
73
+ try destination.writeFloat(value, at: dstIndex)
74
+ }
75
+ }
76
+ }
77
+ }
native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/MLMultiArrayUtilities.swift ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import CoreML
2
+ import Foundation
3
+
4
+ extension MLFeatureProvider {
5
+ func requiredArray(_ name: String) throws -> MLMultiArray {
6
+ guard let value = featureValue(for: name) else {
7
+ throw SuryaRuntimeError.missingFeature(name)
8
+ }
9
+ guard let array = value.multiArrayValue else {
10
+ throw SuryaRuntimeError.missingFeature("\(name) is not an MLMultiArray")
11
+ }
12
+ return array
13
+ }
14
+ }
15
+
16
+ extension MLMultiArray {
17
+ func floatPointer() -> UnsafeMutablePointer<Float> {
18
+ dataPointer.bindMemory(to: Float.self, capacity: count)
19
+ }
20
+
21
+ func halfPointer() -> UnsafeMutablePointer<Float16> {
22
+ dataPointer.bindMemory(to: Float16.self, capacity: count)
23
+ }
24
+
25
+ func readFloat(at linearIndex: Int) throws -> Float {
26
+ switch dataType {
27
+ case .float32:
28
+ return floatPointer()[linearIndex]
29
+ case .float16:
30
+ return Float(halfPointer()[linearIndex])
31
+ default:
32
+ throw SuryaRuntimeError.unsupportedDataType("Expected float16/float32, got \(dataType)")
33
+ }
34
+ }
35
+
36
+ func writeFloat(_ value: Float, at linearIndex: Int) throws {
37
+ switch dataType {
38
+ case .float32:
39
+ floatPointer()[linearIndex] = value
40
+ case .float16:
41
+ halfPointer()[linearIndex] = Float16(value)
42
+ default:
43
+ throw SuryaRuntimeError.unsupportedDataType("Expected float16/float32, got \(dataType)")
44
+ }
45
+ }
46
+
47
+ func argmaxLastDimension() throws -> Int32 {
48
+ guard count > 0 else {
49
+ throw SuryaRuntimeError.invalidShape("Cannot argmax empty MLMultiArray")
50
+ }
51
+ let last = shape.last?.intValue ?? count
52
+ guard last > 0, count % last == 0 else {
53
+ throw SuryaRuntimeError.invalidShape("Bad logits shape \(shape)")
54
+ }
55
+ let offset = count - last
56
+ var bestIndex = 0
57
+ var bestValue = try readFloat(at: offset)
58
+ if last > 1 {
59
+ for i in 1..<last {
60
+ let value = try readFloat(at: offset + i)
61
+ if value > bestValue {
62
+ bestValue = value
63
+ bestIndex = i
64
+ }
65
+ }
66
+ }
67
+ return Int32(bestIndex)
68
+ }
69
+ }
70
+
71
+ func product(_ shape: [Int]) -> Int {
72
+ shape.reduce(1, *)
73
+ }
74
+
75
+ func numberShape(_ shape: [Int]) -> [NSNumber] {
76
+ shape.map { NSNumber(value: $0) }
77
+ }
78
+
79
+ func contiguousFloat32Array(shape: [Int]) throws -> MLMultiArray {
80
+ try MLMultiArray(shape: numberShape(shape), dataType: .float32)
81
+ }
82
+
83
+ func loadRawFloat16AsFloat32(url: URL, shape: [Int]) throws -> MLMultiArray {
84
+ guard FileManager.default.fileExists(atPath: url.path) else {
85
+ throw SuryaRuntimeError.missingFile(url)
86
+ }
87
+ let data = try Data(contentsOf: url, options: [.mappedIfSafe])
88
+ let expectedScalars = product(shape)
89
+ guard data.count == expectedScalars * MemoryLayout<UInt16>.size else {
90
+ throw SuryaRuntimeError.invalidShape("\(url.lastPathComponent) has \(data.count) bytes, expected \(expectedScalars * 2)")
91
+ }
92
+ let array = try contiguousFloat32Array(shape: shape)
93
+ let dst = array.floatPointer()
94
+ data.withUnsafeBytes { raw in
95
+ let src = raw.bindMemory(to: UInt16.self)
96
+ for index in 0..<expectedScalars {
97
+ dst[index] = Float(Float16(bitPattern: src[index]))
98
+ }
99
+ }
100
+ return array
101
+ }
native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/QwenFixedImagePreprocessor.swift ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import CoreGraphics
2
+ import CoreML
3
+ import Foundation
4
+
5
+ public struct QwenFixedImagePreprocessor: Sendable {
6
+ public let size: Int
7
+ public let patchSize: Int
8
+ public let temporalPatchSize: Int
9
+
10
+ public init(size: Int = 512, patchSize: Int = 16, temporalPatchSize: Int = 2) {
11
+ self.size = size
12
+ self.patchSize = patchSize
13
+ self.temporalPatchSize = temporalPatchSize
14
+ }
15
+
16
+ public func pixelValues(from image: CGImage) throws -> MLMultiArray {
17
+ let rgba = try resizedRGBA(image)
18
+ let patchesPerSide = size / patchSize
19
+ let mergeSize = 2
20
+ let mergedPatchesPerSide = patchesPerSide / mergeSize
21
+ let patchCount = patchesPerSide * patchesPerSide
22
+ let patchWidth = 3 * temporalPatchSize * patchSize * patchSize
23
+ let output = try MLMultiArray(shape: [NSNumber(value: patchCount), NSNumber(value: patchWidth)], dataType: .float32)
24
+ let dst = output.floatPointer()
25
+
26
+ var patchIndex = 0
27
+ for blockY in 0..<mergedPatchesPerSide {
28
+ for blockX in 0..<mergedPatchesPerSide {
29
+ for mergeY in 0..<mergeSize {
30
+ for mergeX in 0..<mergeSize {
31
+ let patchY = blockY * mergeSize + mergeY
32
+ let patchX = blockX * mergeSize + mergeX
33
+ let patchOffset = patchIndex * patchWidth
34
+ var cursor = patchOffset
35
+ for channel in 0..<3 {
36
+ for _ in 0..<temporalPatchSize {
37
+ for y in 0..<patchSize {
38
+ let imageY = patchY * patchSize + y
39
+ for x in 0..<patchSize {
40
+ let imageX = patchX * patchSize + x
41
+ let pixelOffset = (imageY * size + imageX) * 4
42
+ let raw: UInt8
43
+ switch channel {
44
+ case 0: raw = rgba[pixelOffset]
45
+ case 1: raw = rgba[pixelOffset + 1]
46
+ default: raw = rgba[pixelOffset + 2]
47
+ }
48
+ dst[cursor] = (Float(raw) / 255.0 - 0.5) / 0.5
49
+ cursor += 1
50
+ }
51
+ }
52
+ }
53
+ }
54
+ patchIndex += 1
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ return output
61
+ }
62
+
63
+ private func resizedRGBA(_ image: CGImage) throws -> [UInt8] {
64
+ if image.width == size,
65
+ image.height == size,
66
+ image.bitsPerComponent == 8,
67
+ image.bitsPerPixel == 32,
68
+ let dataProvider = image.dataProvider,
69
+ let data = dataProvider.data,
70
+ let source = CFDataGetBytePtr(data) {
71
+ let length = CFDataGetLength(data)
72
+ let bytesPerRow = image.bytesPerRow
73
+ if length >= bytesPerRow * size {
74
+ var pixels = [UInt8](repeating: 255, count: size * size * 4)
75
+ for y in 0..<size {
76
+ let srcRow = source + y * bytesPerRow
77
+ let dstRow = y * size * 4
78
+ for x in 0..<size {
79
+ let srcOffset = x * 4
80
+ let dstOffset = dstRow + x * 4
81
+ pixels[dstOffset] = srcRow[srcOffset]
82
+ pixels[dstOffset + 1] = srcRow[srcOffset + 1]
83
+ pixels[dstOffset + 2] = srcRow[srcOffset + 2]
84
+ pixels[dstOffset + 3] = 255
85
+ }
86
+ }
87
+ return pixels
88
+ }
89
+ }
90
+
91
+ var pixels = [UInt8](repeating: 255, count: size * size * 4)
92
+ let bitmapInfo = CGImageAlphaInfo.noneSkipLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
93
+ guard let colorSpace = CGColorSpace(name: CGColorSpace.linearSRGB) ?? CGColorSpace(name: CGColorSpace.sRGB),
94
+ let context = CGContext(
95
+ data: &pixels,
96
+ width: size,
97
+ height: size,
98
+ bitsPerComponent: 8,
99
+ bytesPerRow: size * 4,
100
+ space: colorSpace,
101
+ bitmapInfo: bitmapInfo
102
+ ) else {
103
+ throw SuryaRuntimeError.invalidShape("Could not create CGContext for image preprocessing")
104
+ }
105
+
106
+ context.interpolationQuality = .none
107
+ context.setFillColor(CGColor(red: 1, green: 1, blue: 1, alpha: 1))
108
+ context.fill(CGRect(x: 0, y: 0, width: size, height: size))
109
+
110
+ let sourceWidth = CGFloat(image.width)
111
+ let sourceHeight = CGFloat(image.height)
112
+ let scale = min(CGFloat(size) / sourceWidth, CGFloat(size) / sourceHeight)
113
+ let drawWidth = sourceWidth * scale
114
+ let drawHeight = sourceHeight * scale
115
+ let drawRect = CGRect(
116
+ x: (CGFloat(size) - drawWidth) / 2,
117
+ y: (CGFloat(size) - drawHeight) / 2,
118
+ width: drawWidth,
119
+ height: drawHeight
120
+ )
121
+ context.saveGState()
122
+ context.translateBy(x: 0, y: CGFloat(size))
123
+ context.scaleBy(x: 1, y: -1)
124
+ context.draw(image, in: drawRect)
125
+ context.restoreGState()
126
+ return pixels
127
+ }
128
+ }
native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/SuryaCoreMLRuntime.swift ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import CoreGraphics
2
+ import CoreML
3
+ import Foundation
4
+
5
+ public enum SuryaRuntimeError: Error, CustomStringConvertible {
6
+ case missingFile(URL)
7
+ case malformedConstants(String)
8
+ case missingFeature(String)
9
+ case unsupportedDataType(String)
10
+ case invalidShape(String)
11
+ case generationLimit(String)
12
+
13
+ public var description: String {
14
+ switch self {
15
+ case .missingFile(let url): return "Missing file: \(url.path)"
16
+ case .malformedConstants(let message): return "Malformed constants: \(message)"
17
+ case .missingFeature(let name): return "Missing CoreML feature: \(name)"
18
+ case .unsupportedDataType(let message): return "Unsupported data type: \(message)"
19
+ case .invalidShape(let message): return "Invalid shape: \(message)"
20
+ case .generationLimit(let message): return "Generation limit: \(message)"
21
+ }
22
+ }
23
+ }
24
+
25
+ public struct SuryaGenerationResult: Sendable {
26
+ public let tokenIDs: [Int32]
27
+ public let text: String?
28
+
29
+ public init(tokenIDs: [Int32], text: String? = nil) {
30
+ self.tokenIDs = tokenIDs
31
+ self.text = text
32
+ }
33
+ }
34
+
35
+ public final class SuryaCoreMLRuntime {
36
+ private let bundle: SuryaModelBundle
37
+ private let constants: SuryaNativeConstants
38
+ private let arrays: SuryaNativeArrays
39
+ private let preprocessor: QwenFixedImagePreprocessor
40
+ private let tokenDecoder: TokenDecoder
41
+
42
+ public init(modelDirectory: URL, computeUnits: MLComputeUnits = .cpuOnly) throws {
43
+ self.bundle = try SuryaModelBundle(modelDirectory: modelDirectory, computeUnits: computeUnits)
44
+ self.constants = try SuryaNativeConstants.load(from: modelDirectory.appendingPathComponent("native_assets/surya_native_constants.json"))
45
+ self.arrays = try SuryaNativeArrays(modelDirectory: modelDirectory, constants: constants)
46
+ self.preprocessor = QwenFixedImagePreprocessor()
47
+ self.tokenDecoder = try TokenDecoder(modelDirectory: modelDirectory)
48
+ }
49
+
50
+ public func generate(
51
+ image: CGImage,
52
+ maxNewTokens: Int = 128,
53
+ useInt8Vision: Bool = true,
54
+ stopTokenIDs: Set<Int32> = []
55
+ ) throws -> SuryaGenerationResult {
56
+ let pixelValues = try preprocessor.pixelValues(from: image)
57
+ return try generate(pixelValues: pixelValues, maxNewTokens: maxNewTokens, useInt8Vision: useInt8Vision, stopTokenIDs: stopTokenIDs)
58
+ }
59
+
60
+ public func generate(
61
+ pixelValues: MLMultiArray,
62
+ maxNewTokens: Int = 128,
63
+ useInt8Vision: Bool = true,
64
+ stopTokenIDs: Set<Int32> = []
65
+ ) throws -> SuryaGenerationResult {
66
+ let vision = useInt8Vision ? bundle.visionInt8 : bundle.visionFP16
67
+ let visionOutput = try vision.prediction(from: ["pixel_values": MLFeatureValue(multiArray: pixelValues)])
68
+ let imageEmbeds = try visionOutput.requiredArray("image_embeds")
69
+
70
+ let prefillInputs = try buildPrefillInputs(imageEmbeds: imageEmbeds)
71
+ let prefillOutput = try bundle.prefill.prediction(from: prefillInputs)
72
+ var generated: [Int32] = [try argmax(featureProvider: prefillOutput, name: "logits")]
73
+
74
+ let state = try DecodeState(prefillOutput: prefillOutput, promptLength: constants.promptLength)
75
+ var cacheLength = constants.promptLength
76
+ var currentToken = generated[0]
77
+
78
+ for _ in 0..<maxNewTokens {
79
+ if stopTokenIDs.contains(currentToken) {
80
+ break
81
+ }
82
+ if cacheLength >= constants.maxCacheLength {
83
+ throw SuryaRuntimeError.generationLimit("cacheLength \(cacheLength) reached max \(constants.maxCacheLength)")
84
+ }
85
+
86
+ let decodeInputs = try buildDecodeInputs(currentToken: currentToken, cacheLength: cacheLength, state: state)
87
+ let decodeOutput = try bundle.decodeStep.prediction(from: decodeInputs)
88
+ let nextToken = try argmax(featureProvider: decodeOutput, name: "logits")
89
+
90
+ try state.apply(decodeOutput: decodeOutput, cacheIndex: cacheLength)
91
+ cacheLength += 1
92
+ currentToken = nextToken
93
+ generated.append(nextToken)
94
+ }
95
+
96
+ return SuryaGenerationResult(tokenIDs: generated, text: tokenDecoder.decode(generated))
97
+ }
98
+
99
+ private func buildPrefillInputs(imageEmbeds: MLMultiArray) throws -> [String: MLFeatureValue] {
100
+ let inputsEmbeds = try arrays.makePrefillInputsEmbeds(imageEmbeds: imageEmbeds, imageTokenIndices: constants.imageTokenIndices)
101
+ return [
102
+ "inputs_embeds": MLFeatureValue(multiArray: inputsEmbeds),
103
+ "cos": MLFeatureValue(multiArray: arrays.prefillCos),
104
+ "sin": MLFeatureValue(multiArray: arrays.prefillSin),
105
+ ]
106
+ }
107
+
108
+ private func buildDecodeInputs(currentToken: Int32, cacheLength: Int, state: DecodeState) throws -> [String: MLFeatureValue] {
109
+ var features: [String: MLFeatureValue] = [
110
+ "inputs_embeds": MLFeatureValue(multiArray: try arrays.embedding(for: currentToken)),
111
+ "cos": MLFeatureValue(multiArray: try arrays.decodeCos(position: cacheLength)),
112
+ "sin": MLFeatureValue(multiArray: try arrays.decodeSin(position: cacheLength)),
113
+ "attention_mask": MLFeatureValue(multiArray: try attentionMask(cacheLength: cacheLength)),
114
+ ]
115
+
116
+ for index in 0..<SuryaConstants.fullAttentionLayers {
117
+ features["full_key_\(index)"] = MLFeatureValue(multiArray: state.fullKeys[index])
118
+ features["full_value_\(index)"] = MLFeatureValue(multiArray: state.fullValues[index])
119
+ }
120
+ for index in 0..<SuryaConstants.linearAttentionLayers {
121
+ features["conv_state_\(index)"] = MLFeatureValue(multiArray: state.convStates[index])
122
+ features["recurrent_state_\(index)"] = MLFeatureValue(multiArray: state.recurrentStates[index])
123
+ }
124
+ return features
125
+ }
126
+
127
+ private func attentionMask(cacheLength: Int) throws -> MLMultiArray {
128
+ let width = constants.maxCacheLength + 1
129
+ let array = try MLMultiArray(shape: [1, 1, 1, NSNumber(value: width)], dataType: .float32)
130
+ let values = array.floatPointer()
131
+ let negative = -Float.greatestFiniteMagnitude
132
+ for index in 0..<width {
133
+ values[index] = negative
134
+ }
135
+ for index in 0..<cacheLength {
136
+ values[index] = 0
137
+ }
138
+ values[width - 1] = 0
139
+ return array
140
+ }
141
+
142
+ private func argmax(featureProvider: MLFeatureProvider, name: String) throws -> Int32 {
143
+ let logits = try featureProvider.requiredArray(name)
144
+ return try logits.argmaxLastDimension()
145
+ }
146
+ }
147
+
148
+ enum SuryaConstants {
149
+ static let hiddenSize = 1024
150
+ static let fullAttentionLayers = 6
151
+ static let linearAttentionLayers = 18
152
+ }
native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/SuryaModelBundle.swift ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import CoreML
2
+ import Foundation
3
+
4
+ final class SuryaModelBundle {
5
+ let visionFP16: MLModel
6
+ let visionInt8: MLModel
7
+ let prefill: MLModel
8
+ let decodeStep: MLModel
9
+
10
+ init(modelDirectory: URL, computeUnits: MLComputeUnits) throws {
11
+ let configuration = MLModelConfiguration()
12
+ configuration.computeUnits = computeUnits
13
+
14
+ self.visionFP16 = try Self.loadPackage("surya_vision_fp16.mlpackage", in: modelDirectory, configuration: configuration)
15
+ self.visionInt8 = try Self.loadPackage("surya_vision_int8.mlpackage", in: modelDirectory, configuration: configuration)
16
+ self.prefill = try Self.loadPackage("surya_prefill_fp16_seq300_cache512.mlpackage", in: modelDirectory, configuration: configuration)
17
+ self.decodeStep = try Self.loadPackage("surya_decode_step_fp16_cache512.mlpackage", in: modelDirectory, configuration: configuration)
18
+ }
19
+
20
+ private static func loadPackage(_ name: String, in directory: URL, configuration: MLModelConfiguration) throws -> MLModel {
21
+ let url = directory.appendingPathComponent(name)
22
+ guard FileManager.default.fileExists(atPath: url.path) else {
23
+ throw SuryaRuntimeError.missingFile(url)
24
+ }
25
+ let compiledURL = try MLModel.compileModel(at: url)
26
+ return try MLModel(contentsOf: compiledURL, configuration: configuration)
27
+ }
28
+ }
29
+
30
+ extension MLModel {
31
+ func prediction(from features: [String: MLFeatureValue]) throws -> MLFeatureProvider {
32
+ let provider = try MLDictionaryFeatureProvider(dictionary: features)
33
+ return try prediction(from: provider)
34
+ }
35
+ }
native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/SuryaNativeArrays.swift ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import CoreML
2
+ import Foundation
3
+
4
+ final class SuryaNativeArrays {
5
+ private let constants: SuryaNativeConstants
6
+ let prefillTextEmbedsBase: MLMultiArray
7
+ let prefillCos: MLMultiArray
8
+ let prefillSin: MLMultiArray
9
+ private let decodeCosTable: MLMultiArray
10
+ private let decodeSinTable: MLMultiArray
11
+ private let tokenEmbedding: MLMultiArray
12
+
13
+ init(modelDirectory: URL, constants: SuryaNativeConstants) throws {
14
+ self.constants = constants
15
+ let assets = modelDirectory.appendingPathComponent("native_assets")
16
+ self.prefillTextEmbedsBase = try loadRawFloat16AsFloat32(
17
+ url: assets.appendingPathComponent(constants.files.prefillTextEmbedsBase),
18
+ shape: constants.shapes.prefillTextEmbedsBase
19
+ )
20
+ self.prefillCos = try loadRawFloat16AsFloat32(
21
+ url: assets.appendingPathComponent(constants.files.prefillCos),
22
+ shape: constants.shapes.prefillCos
23
+ )
24
+ self.prefillSin = try loadRawFloat16AsFloat32(
25
+ url: assets.appendingPathComponent(constants.files.prefillSin),
26
+ shape: constants.shapes.prefillSin
27
+ )
28
+ self.decodeCosTable = try loadRawFloat16AsFloat32(
29
+ url: assets.appendingPathComponent(constants.files.decodeCos),
30
+ shape: constants.shapes.decodeCos
31
+ )
32
+ self.decodeSinTable = try loadRawFloat16AsFloat32(
33
+ url: assets.appendingPathComponent(constants.files.decodeSin),
34
+ shape: constants.shapes.decodeSin
35
+ )
36
+ self.tokenEmbedding = try loadRawFloat16AsFloat32(
37
+ url: assets.appendingPathComponent(constants.files.tokenEmbedding),
38
+ shape: constants.shapes.tokenEmbedding
39
+ )
40
+ }
41
+
42
+ func makePrefillInputsEmbeds(imageEmbeds: MLMultiArray, imageTokenIndices: [Int]) throws -> MLMultiArray {
43
+ let shape = constants.shapes.prefillTextEmbedsBase
44
+ guard shape == [1, constants.promptLength, constants.hiddenSize] else {
45
+ throw SuryaRuntimeError.invalidShape("Unexpected prefill base shape \(shape)")
46
+ }
47
+ guard imageTokenIndices.count == constants.shapes.imageEmbeds[0] else {
48
+ throw SuryaRuntimeError.invalidShape("Image token count \(imageTokenIndices.count) does not match image embeds shape \(constants.shapes.imageEmbeds)")
49
+ }
50
+
51
+ let result = try contiguousFloat32Array(shape: shape)
52
+ let srcBase = prefillTextEmbedsBase.floatPointer()
53
+ let dst = result.floatPointer()
54
+ for index in 0..<prefillTextEmbedsBase.count {
55
+ dst[index] = srcBase[index]
56
+ }
57
+
58
+ let hidden = constants.hiddenSize
59
+ for imageIndex in 0..<imageTokenIndices.count {
60
+ let tokenIndex = imageTokenIndices[imageIndex]
61
+ let dstOffset = tokenIndex * hidden
62
+ let srcOffset = imageIndex * hidden
63
+ for hiddenIndex in 0..<hidden {
64
+ dst[dstOffset + hiddenIndex] = try imageEmbeds.readFloat(at: srcOffset + hiddenIndex)
65
+ }
66
+ }
67
+ return result
68
+ }
69
+
70
+ func embedding(for tokenID: Int32) throws -> MLMultiArray {
71
+ let token = Int(tokenID)
72
+ guard token >= 0, token < constants.vocabSize else {
73
+ throw SuryaRuntimeError.invalidShape("Token id \(tokenID) outside vocab \(constants.vocabSize)")
74
+ }
75
+ let hidden = constants.hiddenSize
76
+ let result = try contiguousFloat32Array(shape: [1, 1, hidden])
77
+ let src = tokenEmbedding.floatPointer()
78
+ let dst = result.floatPointer()
79
+ let offset = token * hidden
80
+ for i in 0..<hidden {
81
+ dst[i] = src[offset + i]
82
+ }
83
+ return result
84
+ }
85
+
86
+ func decodeCos(position: Int) throws -> MLMultiArray {
87
+ try decodeTableSlice(decodeCosTable, position: position)
88
+ }
89
+
90
+ func decodeSin(position: Int) throws -> MLMultiArray {
91
+ try decodeTableSlice(decodeSinTable, position: position)
92
+ }
93
+
94
+ private func decodeTableSlice(_ table: MLMultiArray, position: Int) throws -> MLMultiArray {
95
+ let shape = constants.shapes.decodeCos
96
+ guard shape.count == 3, position >= 0, position < shape[1] else {
97
+ throw SuryaRuntimeError.invalidShape("Bad decode position \(position) for shape \(shape)")
98
+ }
99
+ let width = shape[2]
100
+ let result = try contiguousFloat32Array(shape: [1, 1, width])
101
+ let src = table.floatPointer()
102
+ let dst = result.floatPointer()
103
+ let offset = position * width
104
+ for i in 0..<width {
105
+ dst[i] = src[offset + i]
106
+ }
107
+ return result
108
+ }
109
+ }
native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/SuryaNativeConstants.swift ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Foundation
2
+
3
+ struct SuryaNativeConstants: Decodable {
4
+ struct ShapeSet: Decodable {
5
+ let prefillTextEmbedsBase: [Int]
6
+ let prefillCos: [Int]
7
+ let prefillSin: [Int]
8
+ let decodeCos: [Int]
9
+ let decodeSin: [Int]
10
+ let tokenEmbedding: [Int]
11
+ let pixelValues: [Int]
12
+ let imageEmbeds: [Int]
13
+
14
+ enum CodingKeys: String, CodingKey {
15
+ case prefillTextEmbedsBase = "prefill_text_embeds_base"
16
+ case prefillCos = "prefill_cos"
17
+ case prefillSin = "prefill_sin"
18
+ case decodeCos = "decode_cos"
19
+ case decodeSin = "decode_sin"
20
+ case tokenEmbedding = "token_embedding"
21
+ case pixelValues = "pixel_values"
22
+ case imageEmbeds = "image_embeds"
23
+ }
24
+ }
25
+
26
+ struct FileSet: Decodable {
27
+ let prefillTextEmbedsBase: String
28
+ let prefillCos: String
29
+ let prefillSin: String
30
+ let decodeCos: String
31
+ let decodeSin: String
32
+ let tokenEmbedding: String
33
+
34
+ enum CodingKeys: String, CodingKey {
35
+ case prefillTextEmbedsBase = "prefill_text_embeds_base"
36
+ case prefillCos = "prefill_cos"
37
+ case prefillSin = "prefill_sin"
38
+ case decodeCos = "decode_cos"
39
+ case decodeSin = "decode_sin"
40
+ case tokenEmbedding = "token_embedding"
41
+ }
42
+ }
43
+
44
+ let modelID: String
45
+ let prompt: String
46
+ let inputIDs: [Int32]
47
+ let attentionMask: [Int]
48
+ let mmTokenTypeIDs: [Int]
49
+ let imageGridTHW: [Int]
50
+ let imageTokenIndices: [Int]
51
+ let shapes: ShapeSet
52
+ let files: FileSet
53
+
54
+ enum CodingKeys: String, CodingKey {
55
+ case modelID = "model_id"
56
+ case prompt
57
+ case inputIDs = "input_ids"
58
+ case attentionMask = "attention_mask"
59
+ case mmTokenTypeIDs = "mm_token_type_ids"
60
+ case imageGridTHW = "image_grid_thw"
61
+ case imageTokenIndices = "image_token_indices"
62
+ case shapes
63
+ case files
64
+ }
65
+
66
+ var promptLength: Int { inputIDs.count }
67
+ var maxCacheLength: Int { shapes.decodeCos.count >= 2 ? shapes.decodeCos[1] : 512 }
68
+ var vocabSize: Int { shapes.tokenEmbedding[0] }
69
+ var hiddenSize: Int { shapes.tokenEmbedding[1] }
70
+
71
+ static func load(from url: URL) throws -> SuryaNativeConstants {
72
+ guard FileManager.default.fileExists(atPath: url.path) else {
73
+ throw SuryaRuntimeError.missingFile(url)
74
+ }
75
+ let data = try Data(contentsOf: url)
76
+ let decoder = JSONDecoder()
77
+ do {
78
+ return try decoder.decode(SuryaNativeConstants.self, from: data)
79
+ } catch {
80
+ throw SuryaRuntimeError.malformedConstants(error.localizedDescription)
81
+ }
82
+ }
83
+ }
native/SuryaCoreMLRuntime/Sources/SuryaCoreMLRuntime/TokenDecoder.swift ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Foundation
2
+
3
+ final class TokenDecoder {
4
+ private let idToToken: [String]
5
+
6
+ init(modelDirectory: URL) throws {
7
+ let url = modelDirectory.appendingPathComponent("native_assets/id_to_token.json")
8
+ guard FileManager.default.fileExists(atPath: url.path) else {
9
+ throw SuryaRuntimeError.missingFile(url)
10
+ }
11
+ let data = try Data(contentsOf: url, options: [.mappedIfSafe])
12
+ self.idToToken = try JSONDecoder().decode([String].self, from: data)
13
+ }
14
+
15
+ func decode(_ tokenIDs: [Int32]) -> String {
16
+ var output = ""
17
+ var byteBuffer: [UInt8] = []
18
+
19
+ func flushBytes() {
20
+ guard !byteBuffer.isEmpty else { return }
21
+ output += String(decoding: byteBuffer, as: UTF8.self)
22
+ byteBuffer.removeAll(keepingCapacity: true)
23
+ }
24
+
25
+ for id in tokenIDs {
26
+ let index = Int(id)
27
+ guard index >= 0, index < idToToken.count else { continue }
28
+ let token = idToToken[index]
29
+ if token.isEmpty { continue }
30
+
31
+ if let byte = Self.byteFallback(token) {
32
+ byteBuffer.append(byte)
33
+ continue
34
+ }
35
+
36
+ flushBytes()
37
+ output += Self.cleanToken(token)
38
+ }
39
+
40
+ flushBytes()
41
+ return output
42
+ }
43
+
44
+ private static func cleanToken(_ token: String) -> String {
45
+ if token == "Ġ" || token == "▁" {
46
+ return " "
47
+ }
48
+ var value = token
49
+ if value.hasPrefix("Ġ") || value.hasPrefix("▁") {
50
+ value.removeFirst()
51
+ value = " " + value
52
+ }
53
+ value = value.replacingOccurrences(of: "Ċ", with: "\n")
54
+ return value
55
+ }
56
+
57
+ private static func byteFallback(_ token: String) -> UInt8? {
58
+ guard token.hasPrefix("<0x"), token.hasSuffix(">") else {
59
+ return nil
60
+ }
61
+ let start = token.index(token.startIndex, offsetBy: 3)
62
+ let end = token.index(before: token.endIndex)
63
+ return UInt8(token[start..<end], radix: 16)
64
+ }
65
+ }
native/SuryaCoreMLRuntime/Sources/SuryaCoreMLSmoke/main.swift ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import CoreGraphics
2
+ import CoreML
3
+ import Foundation
4
+ import ImageIO
5
+ import SuryaCoreMLRuntime
6
+
7
+ struct Arguments {
8
+ let modelDirectory: URL
9
+ let imageURL: URL?
10
+ let pixelValuesURL: URL?
11
+ let dumpPixelValuesURL: URL?
12
+ let maxTokens: Int
13
+ let useInt8Vision: Bool
14
+ }
15
+
16
+ func parseArguments() throws -> Arguments {
17
+ var modelDirectory: URL?
18
+ var imageURL: URL?
19
+ var pixelValuesURL: URL?
20
+ var dumpPixelValuesURL: URL?
21
+ var maxTokens = 32
22
+ var useInt8Vision = true
23
+
24
+ var iterator = CommandLine.arguments.dropFirst().makeIterator()
25
+ while let arg = iterator.next() {
26
+ switch arg {
27
+ case "--model-dir":
28
+ guard let value = iterator.next() else { throw SuryaRuntimeError.malformedConstants("Missing --model-dir value") }
29
+ modelDirectory = URL(fileURLWithPath: value)
30
+ case "--image":
31
+ guard let value = iterator.next() else { throw SuryaRuntimeError.malformedConstants("Missing --image value") }
32
+ imageURL = URL(fileURLWithPath: value)
33
+ case "--pixel-values-fp16":
34
+ guard let value = iterator.next() else { throw SuryaRuntimeError.malformedConstants("Missing --pixel-values-fp16 value") }
35
+ pixelValuesURL = URL(fileURLWithPath: value)
36
+ case "--dump-pixel-values-fp16":
37
+ guard let value = iterator.next() else { throw SuryaRuntimeError.malformedConstants("Missing --dump-pixel-values-fp16 value") }
38
+ dumpPixelValuesURL = URL(fileURLWithPath: value)
39
+ case "--max-tokens":
40
+ guard let value = iterator.next(), let parsed = Int(value) else {
41
+ throw SuryaRuntimeError.malformedConstants("Bad --max-tokens value")
42
+ }
43
+ maxTokens = parsed
44
+ case "--vision":
45
+ guard let value = iterator.next() else { throw SuryaRuntimeError.malformedConstants("Missing --vision value") }
46
+ useInt8Vision = value.lowercased() != "fp16"
47
+ default:
48
+ throw SuryaRuntimeError.malformedConstants("Unknown argument \(arg)")
49
+ }
50
+ }
51
+
52
+ guard let modelDirectory else {
53
+ throw SuryaRuntimeError.malformedConstants("Usage: surya-coreml-smoke --model-dir /path/to/repo --image /path/to/image [--max-tokens 32] [--vision int8|fp16]")
54
+ }
55
+ if imageURL == nil && pixelValuesURL == nil {
56
+ throw SuryaRuntimeError.malformedConstants("Pass either --image or --pixel-values-fp16")
57
+ }
58
+
59
+ return Arguments(
60
+ modelDirectory: modelDirectory,
61
+ imageURL: imageURL,
62
+ pixelValuesURL: pixelValuesURL,
63
+ dumpPixelValuesURL: dumpPixelValuesURL,
64
+ maxTokens: maxTokens,
65
+ useInt8Vision: useInt8Vision
66
+ )
67
+ }
68
+
69
+ func loadImage(_ url: URL) throws -> CGImage {
70
+ guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
71
+ let image = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
72
+ throw SuryaRuntimeError.missingFile(url)
73
+ }
74
+ return image
75
+ }
76
+
77
+ func loadPixelValues(_ url: URL) throws -> MLMultiArray {
78
+ let data = try Data(contentsOf: url, options: [.mappedIfSafe])
79
+ let shape = [1024, 1536]
80
+ let expected = shape.reduce(1, *) * MemoryLayout<UInt16>.size
81
+ guard data.count == expected else {
82
+ throw SuryaRuntimeError.invalidShape("Expected \(expected) bytes, got \(data.count)")
83
+ }
84
+ let array = try MLMultiArray(shape: shape.map(NSNumber.init(value:)), dataType: .float32)
85
+ let dst = array.dataPointer.bindMemory(to: Float.self, capacity: shape.reduce(1, *))
86
+ data.withUnsafeBytes { raw in
87
+ let src = raw.bindMemory(to: UInt16.self)
88
+ for i in 0..<src.count {
89
+ dst[i] = Float(Float16(bitPattern: src[i]))
90
+ }
91
+ }
92
+ return array
93
+ }
94
+
95
+ func dumpPixelValues(_ array: MLMultiArray, to url: URL) throws {
96
+ var data = Data(capacity: array.count * MemoryLayout<UInt16>.size)
97
+ for index in 0..<array.count {
98
+ let value = array.dataPointer.bindMemory(to: Float.self, capacity: array.count)[index]
99
+ var bits = Float16(value).bitPattern
100
+ withUnsafeBytes(of: &bits) { data.append(contentsOf: $0) }
101
+ }
102
+ try data.write(to: url)
103
+ }
104
+
105
+ do {
106
+ let args = try parseArguments()
107
+ let runtime = try SuryaCoreMLRuntime(modelDirectory: args.modelDirectory)
108
+ let result: SuryaGenerationResult
109
+ if let pixelValuesURL = args.pixelValuesURL {
110
+ let pixelValues = try loadPixelValues(pixelValuesURL)
111
+ result = try runtime.generate(pixelValues: pixelValues, maxNewTokens: args.maxTokens, useInt8Vision: args.useInt8Vision)
112
+ } else if let imageURL = args.imageURL {
113
+ let image = try loadImage(imageURL)
114
+ let preprocessor = QwenFixedImagePreprocessor()
115
+ let pixelValues = try preprocessor.pixelValues(from: image)
116
+ if let dumpURL = args.dumpPixelValuesURL {
117
+ try dumpPixelValues(pixelValues, to: dumpURL)
118
+ }
119
+ result = try runtime.generate(pixelValues: pixelValues, maxNewTokens: args.maxTokens, useInt8Vision: args.useInt8Vision)
120
+ } else {
121
+ throw SuryaRuntimeError.malformedConstants("Pass either --image or --pixel-values-fp16")
122
+ }
123
+ print(result.tokenIDs.map(String.init).joined(separator: " "))
124
+ if let text = result.text {
125
+ print(text)
126
+ }
127
+ } catch {
128
+ fputs("surya-coreml-smoke failed: \(error)\n", stderr)
129
+ exit(1)
130
+ }
native/SuryaCoreMLRuntime/Tests/SuryaCoreMLRuntimeTests/SuryaCoreMLRuntimeTests.swift ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import CoreML
2
+ import Testing
3
+ @testable import SuryaCoreMLRuntime
4
+
5
+ @Test func qwenFixedImagePreprocessorShape() throws {
6
+ let width = 32
7
+ let height = 32
8
+ var pixels = [UInt8](repeating: 255, count: width * height * 4)
9
+ for i in stride(from: 0, to: pixels.count, by: 4) {
10
+ pixels[i] = 0
11
+ pixels[i + 1] = 0
12
+ pixels[i + 2] = 0
13
+ pixels[i + 3] = 255
14
+ }
15
+ let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
16
+ let context = CGContext(
17
+ data: &pixels,
18
+ width: width,
19
+ height: height,
20
+ bitsPerComponent: 8,
21
+ bytesPerRow: width * 4,
22
+ space: colorSpace,
23
+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
24
+ )!
25
+ let image = context.makeImage()!
26
+ let values = try QwenFixedImagePreprocessor().pixelValues(from: image)
27
+ #expect(values.shape.map(\.intValue) == [1024, 1536])
28
+ }
native_assets/canary_pixel_values_fp16.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fe20bcb89c45b610cc3912c9f2b83e5bb7e374370fefafbb86ab185ef318fdbe
3
+ size 3145728
native_assets/decode_cos_fp16.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9dfc5caf59d669a7acc909daae1a250a260802b5f123bbedc3038010cdb459c7
3
+ size 65536
native_assets/decode_sin_fp16.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:67f1a0febb226f6521ed34c32c5669c0b8356bcfedd5bf27d27b006ebd63eeab
3
+ size 65536
native_assets/id_to_token.json ADDED
The diff for this file is too large to render. See raw diff
 
native_assets/prefill_cos_fp16.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a44a96a45de3e01c5036dc145460e81a49a40cc101b8fccca93255a5f401bfde
3
+ size 38400
native_assets/prefill_sin_fp16.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:367b5007b74b28b90677d49dcc8813a49d67a2a3e5be8682da42facb1776496c
3
+ size 38400
native_assets/prefill_text_embeds_base_fp16.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4ee2b84e91c671347cc07c2f614821744728b6e2c3938d0cc7f618a41f104147
3
+ size 614400
native_assets/surya_native_constants.json ADDED
@@ -0,0 +1,1222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_id": "datalab-to/surya-ocr-2",
3
+ "prompt": "OCR this image to HTML.",
4
+ "input_ids": [
5
+ 1,
6
+ 2053,
7
+ 2051,
8
+ 2037,
9
+ 2050,
10
+ 1957,
11
+ 9,
12
+ 11,
13
+ 11,
14
+ 11,
15
+ 11,
16
+ 11,
17
+ 11,
18
+ 11,
19
+ 11,
20
+ 11,
21
+ 11,
22
+ 11,
23
+ 11,
24
+ 11,
25
+ 11,
26
+ 11,
27
+ 11,
28
+ 11,
29
+ 11,
30
+ 11,
31
+ 11,
32
+ 11,
33
+ 11,
34
+ 11,
35
+ 11,
36
+ 11,
37
+ 11,
38
+ 11,
39
+ 11,
40
+ 11,
41
+ 11,
42
+ 11,
43
+ 11,
44
+ 11,
45
+ 11,
46
+ 11,
47
+ 11,
48
+ 11,
49
+ 11,
50
+ 11,
51
+ 11,
52
+ 11,
53
+ 11,
54
+ 11,
55
+ 11,
56
+ 11,
57
+ 11,
58
+ 11,
59
+ 11,
60
+ 11,
61
+ 11,
62
+ 11,
63
+ 11,
64
+ 11,
65
+ 11,
66
+ 11,
67
+ 11,
68
+ 11,
69
+ 11,
70
+ 11,
71
+ 11,
72
+ 11,
73
+ 11,
74
+ 11,
75
+ 11,
76
+ 11,
77
+ 11,
78
+ 11,
79
+ 11,
80
+ 11,
81
+ 11,
82
+ 11,
83
+ 11,
84
+ 11,
85
+ 11,
86
+ 11,
87
+ 11,
88
+ 11,
89
+ 11,
90
+ 11,
91
+ 11,
92
+ 11,
93
+ 11,
94
+ 11,
95
+ 11,
96
+ 11,
97
+ 11,
98
+ 11,
99
+ 11,
100
+ 11,
101
+ 11,
102
+ 11,
103
+ 11,
104
+ 11,
105
+ 11,
106
+ 11,
107
+ 11,
108
+ 11,
109
+ 11,
110
+ 11,
111
+ 11,
112
+ 11,
113
+ 11,
114
+ 11,
115
+ 11,
116
+ 11,
117
+ 11,
118
+ 11,
119
+ 11,
120
+ 11,
121
+ 11,
122
+ 11,
123
+ 11,
124
+ 11,
125
+ 11,
126
+ 11,
127
+ 11,
128
+ 11,
129
+ 11,
130
+ 11,
131
+ 11,
132
+ 11,
133
+ 11,
134
+ 11,
135
+ 11,
136
+ 11,
137
+ 11,
138
+ 11,
139
+ 11,
140
+ 11,
141
+ 11,
142
+ 11,
143
+ 11,
144
+ 11,
145
+ 11,
146
+ 11,
147
+ 11,
148
+ 11,
149
+ 11,
150
+ 11,
151
+ 11,
152
+ 11,
153
+ 11,
154
+ 11,
155
+ 11,
156
+ 11,
157
+ 11,
158
+ 11,
159
+ 11,
160
+ 11,
161
+ 11,
162
+ 11,
163
+ 11,
164
+ 11,
165
+ 11,
166
+ 11,
167
+ 11,
168
+ 11,
169
+ 11,
170
+ 11,
171
+ 11,
172
+ 11,
173
+ 11,
174
+ 11,
175
+ 11,
176
+ 11,
177
+ 11,
178
+ 11,
179
+ 11,
180
+ 11,
181
+ 11,
182
+ 11,
183
+ 11,
184
+ 11,
185
+ 11,
186
+ 11,
187
+ 11,
188
+ 11,
189
+ 11,
190
+ 11,
191
+ 11,
192
+ 11,
193
+ 11,
194
+ 11,
195
+ 11,
196
+ 11,
197
+ 11,
198
+ 11,
199
+ 11,
200
+ 11,
201
+ 11,
202
+ 11,
203
+ 11,
204
+ 11,
205
+ 11,
206
+ 11,
207
+ 11,
208
+ 11,
209
+ 11,
210
+ 11,
211
+ 11,
212
+ 11,
213
+ 11,
214
+ 11,
215
+ 11,
216
+ 11,
217
+ 11,
218
+ 11,
219
+ 11,
220
+ 11,
221
+ 11,
222
+ 11,
223
+ 11,
224
+ 11,
225
+ 11,
226
+ 11,
227
+ 11,
228
+ 11,
229
+ 11,
230
+ 11,
231
+ 11,
232
+ 11,
233
+ 11,
234
+ 11,
235
+ 11,
236
+ 11,
237
+ 11,
238
+ 11,
239
+ 11,
240
+ 11,
241
+ 11,
242
+ 11,
243
+ 11,
244
+ 11,
245
+ 11,
246
+ 11,
247
+ 11,
248
+ 11,
249
+ 11,
250
+ 11,
251
+ 11,
252
+ 11,
253
+ 11,
254
+ 11,
255
+ 11,
256
+ 11,
257
+ 11,
258
+ 11,
259
+ 11,
260
+ 11,
261
+ 11,
262
+ 11,
263
+ 11,
264
+ 11,
265
+ 11,
266
+ 11,
267
+ 11,
268
+ 10,
269
+ 2015,
270
+ 2003,
271
+ 2018,
272
+ 1979,
273
+ 2052,
274
+ 2040,
275
+ 2041,
276
+ 2051,
277
+ 1979,
278
+ 2041,
279
+ 2045,
280
+ 2033,
281
+ 2039,
282
+ 2037,
283
+ 1979,
284
+ 2052,
285
+ 2047,
286
+ 1979,
287
+ 2008,
288
+ 2020,
289
+ 2013,
290
+ 2012,
291
+ 1992,
292
+ 2,
293
+ 1957,
294
+ 1,
295
+ 2033,
296
+ 2051,
297
+ 2051,
298
+ 2041,
299
+ 2051,
300
+ 2052,
301
+ 2033,
302
+ 2046,
303
+ 2052,
304
+ 1957
305
+ ],
306
+ "attention_mask": [
307
+ 1,
308
+ 1,
309
+ 1,
310
+ 1,
311
+ 1,
312
+ 1,
313
+ 1,
314
+ 1,
315
+ 1,
316
+ 1,
317
+ 1,
318
+ 1,
319
+ 1,
320
+ 1,
321
+ 1,
322
+ 1,
323
+ 1,
324
+ 1,
325
+ 1,
326
+ 1,
327
+ 1,
328
+ 1,
329
+ 1,
330
+ 1,
331
+ 1,
332
+ 1,
333
+ 1,
334
+ 1,
335
+ 1,
336
+ 1,
337
+ 1,
338
+ 1,
339
+ 1,
340
+ 1,
341
+ 1,
342
+ 1,
343
+ 1,
344
+ 1,
345
+ 1,
346
+ 1,
347
+ 1,
348
+ 1,
349
+ 1,
350
+ 1,
351
+ 1,
352
+ 1,
353
+ 1,
354
+ 1,
355
+ 1,
356
+ 1,
357
+ 1,
358
+ 1,
359
+ 1,
360
+ 1,
361
+ 1,
362
+ 1,
363
+ 1,
364
+ 1,
365
+ 1,
366
+ 1,
367
+ 1,
368
+ 1,
369
+ 1,
370
+ 1,
371
+ 1,
372
+ 1,
373
+ 1,
374
+ 1,
375
+ 1,
376
+ 1,
377
+ 1,
378
+ 1,
379
+ 1,
380
+ 1,
381
+ 1,
382
+ 1,
383
+ 1,
384
+ 1,
385
+ 1,
386
+ 1,
387
+ 1,
388
+ 1,
389
+ 1,
390
+ 1,
391
+ 1,
392
+ 1,
393
+ 1,
394
+ 1,
395
+ 1,
396
+ 1,
397
+ 1,
398
+ 1,
399
+ 1,
400
+ 1,
401
+ 1,
402
+ 1,
403
+ 1,
404
+ 1,
405
+ 1,
406
+ 1,
407
+ 1,
408
+ 1,
409
+ 1,
410
+ 1,
411
+ 1,
412
+ 1,
413
+ 1,
414
+ 1,
415
+ 1,
416
+ 1,
417
+ 1,
418
+ 1,
419
+ 1,
420
+ 1,
421
+ 1,
422
+ 1,
423
+ 1,
424
+ 1,
425
+ 1,
426
+ 1,
427
+ 1,
428
+ 1,
429
+ 1,
430
+ 1,
431
+ 1,
432
+ 1,
433
+ 1,
434
+ 1,
435
+ 1,
436
+ 1,
437
+ 1,
438
+ 1,
439
+ 1,
440
+ 1,
441
+ 1,
442
+ 1,
443
+ 1,
444
+ 1,
445
+ 1,
446
+ 1,
447
+ 1,
448
+ 1,
449
+ 1,
450
+ 1,
451
+ 1,
452
+ 1,
453
+ 1,
454
+ 1,
455
+ 1,
456
+ 1,
457
+ 1,
458
+ 1,
459
+ 1,
460
+ 1,
461
+ 1,
462
+ 1,
463
+ 1,
464
+ 1,
465
+ 1,
466
+ 1,
467
+ 1,
468
+ 1,
469
+ 1,
470
+ 1,
471
+ 1,
472
+ 1,
473
+ 1,
474
+ 1,
475
+ 1,
476
+ 1,
477
+ 1,
478
+ 1,
479
+ 1,
480
+ 1,
481
+ 1,
482
+ 1,
483
+ 1,
484
+ 1,
485
+ 1,
486
+ 1,
487
+ 1,
488
+ 1,
489
+ 1,
490
+ 1,
491
+ 1,
492
+ 1,
493
+ 1,
494
+ 1,
495
+ 1,
496
+ 1,
497
+ 1,
498
+ 1,
499
+ 1,
500
+ 1,
501
+ 1,
502
+ 1,
503
+ 1,
504
+ 1,
505
+ 1,
506
+ 1,
507
+ 1,
508
+ 1,
509
+ 1,
510
+ 1,
511
+ 1,
512
+ 1,
513
+ 1,
514
+ 1,
515
+ 1,
516
+ 1,
517
+ 1,
518
+ 1,
519
+ 1,
520
+ 1,
521
+ 1,
522
+ 1,
523
+ 1,
524
+ 1,
525
+ 1,
526
+ 1,
527
+ 1,
528
+ 1,
529
+ 1,
530
+ 1,
531
+ 1,
532
+ 1,
533
+ 1,
534
+ 1,
535
+ 1,
536
+ 1,
537
+ 1,
538
+ 1,
539
+ 1,
540
+ 1,
541
+ 1,
542
+ 1,
543
+ 1,
544
+ 1,
545
+ 1,
546
+ 1,
547
+ 1,
548
+ 1,
549
+ 1,
550
+ 1,
551
+ 1,
552
+ 1,
553
+ 1,
554
+ 1,
555
+ 1,
556
+ 1,
557
+ 1,
558
+ 1,
559
+ 1,
560
+ 1,
561
+ 1,
562
+ 1,
563
+ 1,
564
+ 1,
565
+ 1,
566
+ 1,
567
+ 1,
568
+ 1,
569
+ 1,
570
+ 1,
571
+ 1,
572
+ 1,
573
+ 1,
574
+ 1,
575
+ 1,
576
+ 1,
577
+ 1,
578
+ 1,
579
+ 1,
580
+ 1,
581
+ 1,
582
+ 1,
583
+ 1,
584
+ 1,
585
+ 1,
586
+ 1,
587
+ 1,
588
+ 1,
589
+ 1,
590
+ 1,
591
+ 1,
592
+ 1,
593
+ 1,
594
+ 1,
595
+ 1,
596
+ 1,
597
+ 1,
598
+ 1,
599
+ 1,
600
+ 1,
601
+ 1,
602
+ 1,
603
+ 1,
604
+ 1,
605
+ 1,
606
+ 1
607
+ ],
608
+ "mm_token_type_ids": [
609
+ 0,
610
+ 0,
611
+ 0,
612
+ 0,
613
+ 0,
614
+ 0,
615
+ 0,
616
+ 1,
617
+ 1,
618
+ 1,
619
+ 1,
620
+ 1,
621
+ 1,
622
+ 1,
623
+ 1,
624
+ 1,
625
+ 1,
626
+ 1,
627
+ 1,
628
+ 1,
629
+ 1,
630
+ 1,
631
+ 1,
632
+ 1,
633
+ 1,
634
+ 1,
635
+ 1,
636
+ 1,
637
+ 1,
638
+ 1,
639
+ 1,
640
+ 1,
641
+ 1,
642
+ 1,
643
+ 1,
644
+ 1,
645
+ 1,
646
+ 1,
647
+ 1,
648
+ 1,
649
+ 1,
650
+ 1,
651
+ 1,
652
+ 1,
653
+ 1,
654
+ 1,
655
+ 1,
656
+ 1,
657
+ 1,
658
+ 1,
659
+ 1,
660
+ 1,
661
+ 1,
662
+ 1,
663
+ 1,
664
+ 1,
665
+ 1,
666
+ 1,
667
+ 1,
668
+ 1,
669
+ 1,
670
+ 1,
671
+ 1,
672
+ 1,
673
+ 1,
674
+ 1,
675
+ 1,
676
+ 1,
677
+ 1,
678
+ 1,
679
+ 1,
680
+ 1,
681
+ 1,
682
+ 1,
683
+ 1,
684
+ 1,
685
+ 1,
686
+ 1,
687
+ 1,
688
+ 1,
689
+ 1,
690
+ 1,
691
+ 1,
692
+ 1,
693
+ 1,
694
+ 1,
695
+ 1,
696
+ 1,
697
+ 1,
698
+ 1,
699
+ 1,
700
+ 1,
701
+ 1,
702
+ 1,
703
+ 1,
704
+ 1,
705
+ 1,
706
+ 1,
707
+ 1,
708
+ 1,
709
+ 1,
710
+ 1,
711
+ 1,
712
+ 1,
713
+ 1,
714
+ 1,
715
+ 1,
716
+ 1,
717
+ 1,
718
+ 1,
719
+ 1,
720
+ 1,
721
+ 1,
722
+ 1,
723
+ 1,
724
+ 1,
725
+ 1,
726
+ 1,
727
+ 1,
728
+ 1,
729
+ 1,
730
+ 1,
731
+ 1,
732
+ 1,
733
+ 1,
734
+ 1,
735
+ 1,
736
+ 1,
737
+ 1,
738
+ 1,
739
+ 1,
740
+ 1,
741
+ 1,
742
+ 1,
743
+ 1,
744
+ 1,
745
+ 1,
746
+ 1,
747
+ 1,
748
+ 1,
749
+ 1,
750
+ 1,
751
+ 1,
752
+ 1,
753
+ 1,
754
+ 1,
755
+ 1,
756
+ 1,
757
+ 1,
758
+ 1,
759
+ 1,
760
+ 1,
761
+ 1,
762
+ 1,
763
+ 1,
764
+ 1,
765
+ 1,
766
+ 1,
767
+ 1,
768
+ 1,
769
+ 1,
770
+ 1,
771
+ 1,
772
+ 1,
773
+ 1,
774
+ 1,
775
+ 1,
776
+ 1,
777
+ 1,
778
+ 1,
779
+ 1,
780
+ 1,
781
+ 1,
782
+ 1,
783
+ 1,
784
+ 1,
785
+ 1,
786
+ 1,
787
+ 1,
788
+ 1,
789
+ 1,
790
+ 1,
791
+ 1,
792
+ 1,
793
+ 1,
794
+ 1,
795
+ 1,
796
+ 1,
797
+ 1,
798
+ 1,
799
+ 1,
800
+ 1,
801
+ 1,
802
+ 1,
803
+ 1,
804
+ 1,
805
+ 1,
806
+ 1,
807
+ 1,
808
+ 1,
809
+ 1,
810
+ 1,
811
+ 1,
812
+ 1,
813
+ 1,
814
+ 1,
815
+ 1,
816
+ 1,
817
+ 1,
818
+ 1,
819
+ 1,
820
+ 1,
821
+ 1,
822
+ 1,
823
+ 1,
824
+ 1,
825
+ 1,
826
+ 1,
827
+ 1,
828
+ 1,
829
+ 1,
830
+ 1,
831
+ 1,
832
+ 1,
833
+ 1,
834
+ 1,
835
+ 1,
836
+ 1,
837
+ 1,
838
+ 1,
839
+ 1,
840
+ 1,
841
+ 1,
842
+ 1,
843
+ 1,
844
+ 1,
845
+ 1,
846
+ 1,
847
+ 1,
848
+ 1,
849
+ 1,
850
+ 1,
851
+ 1,
852
+ 1,
853
+ 1,
854
+ 1,
855
+ 1,
856
+ 1,
857
+ 1,
858
+ 1,
859
+ 1,
860
+ 1,
861
+ 1,
862
+ 1,
863
+ 1,
864
+ 1,
865
+ 1,
866
+ 1,
867
+ 1,
868
+ 1,
869
+ 1,
870
+ 1,
871
+ 1,
872
+ 0,
873
+ 0,
874
+ 0,
875
+ 0,
876
+ 0,
877
+ 0,
878
+ 0,
879
+ 0,
880
+ 0,
881
+ 0,
882
+ 0,
883
+ 0,
884
+ 0,
885
+ 0,
886
+ 0,
887
+ 0,
888
+ 0,
889
+ 0,
890
+ 0,
891
+ 0,
892
+ 0,
893
+ 0,
894
+ 0,
895
+ 0,
896
+ 0,
897
+ 0,
898
+ 0,
899
+ 0,
900
+ 0,
901
+ 0,
902
+ 0,
903
+ 0,
904
+ 0,
905
+ 0,
906
+ 0,
907
+ 0,
908
+ 0
909
+ ],
910
+ "image_grid_thw": [
911
+ 1,
912
+ 32,
913
+ 32
914
+ ],
915
+ "image_token_indices": [
916
+ 7,
917
+ 8,
918
+ 9,
919
+ 10,
920
+ 11,
921
+ 12,
922
+ 13,
923
+ 14,
924
+ 15,
925
+ 16,
926
+ 17,
927
+ 18,
928
+ 19,
929
+ 20,
930
+ 21,
931
+ 22,
932
+ 23,
933
+ 24,
934
+ 25,
935
+ 26,
936
+ 27,
937
+ 28,
938
+ 29,
939
+ 30,
940
+ 31,
941
+ 32,
942
+ 33,
943
+ 34,
944
+ 35,
945
+ 36,
946
+ 37,
947
+ 38,
948
+ 39,
949
+ 40,
950
+ 41,
951
+ 42,
952
+ 43,
953
+ 44,
954
+ 45,
955
+ 46,
956
+ 47,
957
+ 48,
958
+ 49,
959
+ 50,
960
+ 51,
961
+ 52,
962
+ 53,
963
+ 54,
964
+ 55,
965
+ 56,
966
+ 57,
967
+ 58,
968
+ 59,
969
+ 60,
970
+ 61,
971
+ 62,
972
+ 63,
973
+ 64,
974
+ 65,
975
+ 66,
976
+ 67,
977
+ 68,
978
+ 69,
979
+ 70,
980
+ 71,
981
+ 72,
982
+ 73,
983
+ 74,
984
+ 75,
985
+ 76,
986
+ 77,
987
+ 78,
988
+ 79,
989
+ 80,
990
+ 81,
991
+ 82,
992
+ 83,
993
+ 84,
994
+ 85,
995
+ 86,
996
+ 87,
997
+ 88,
998
+ 89,
999
+ 90,
1000
+ 91,
1001
+ 92,
1002
+ 93,
1003
+ 94,
1004
+ 95,
1005
+ 96,
1006
+ 97,
1007
+ 98,
1008
+ 99,
1009
+ 100,
1010
+ 101,
1011
+ 102,
1012
+ 103,
1013
+ 104,
1014
+ 105,
1015
+ 106,
1016
+ 107,
1017
+ 108,
1018
+ 109,
1019
+ 110,
1020
+ 111,
1021
+ 112,
1022
+ 113,
1023
+ 114,
1024
+ 115,
1025
+ 116,
1026
+ 117,
1027
+ 118,
1028
+ 119,
1029
+ 120,
1030
+ 121,
1031
+ 122,
1032
+ 123,
1033
+ 124,
1034
+ 125,
1035
+ 126,
1036
+ 127,
1037
+ 128,
1038
+ 129,
1039
+ 130,
1040
+ 131,
1041
+ 132,
1042
+ 133,
1043
+ 134,
1044
+ 135,
1045
+ 136,
1046
+ 137,
1047
+ 138,
1048
+ 139,
1049
+ 140,
1050
+ 141,
1051
+ 142,
1052
+ 143,
1053
+ 144,
1054
+ 145,
1055
+ 146,
1056
+ 147,
1057
+ 148,
1058
+ 149,
1059
+ 150,
1060
+ 151,
1061
+ 152,
1062
+ 153,
1063
+ 154,
1064
+ 155,
1065
+ 156,
1066
+ 157,
1067
+ 158,
1068
+ 159,
1069
+ 160,
1070
+ 161,
1071
+ 162,
1072
+ 163,
1073
+ 164,
1074
+ 165,
1075
+ 166,
1076
+ 167,
1077
+ 168,
1078
+ 169,
1079
+ 170,
1080
+ 171,
1081
+ 172,
1082
+ 173,
1083
+ 174,
1084
+ 175,
1085
+ 176,
1086
+ 177,
1087
+ 178,
1088
+ 179,
1089
+ 180,
1090
+ 181,
1091
+ 182,
1092
+ 183,
1093
+ 184,
1094
+ 185,
1095
+ 186,
1096
+ 187,
1097
+ 188,
1098
+ 189,
1099
+ 190,
1100
+ 191,
1101
+ 192,
1102
+ 193,
1103
+ 194,
1104
+ 195,
1105
+ 196,
1106
+ 197,
1107
+ 198,
1108
+ 199,
1109
+ 200,
1110
+ 201,
1111
+ 202,
1112
+ 203,
1113
+ 204,
1114
+ 205,
1115
+ 206,
1116
+ 207,
1117
+ 208,
1118
+ 209,
1119
+ 210,
1120
+ 211,
1121
+ 212,
1122
+ 213,
1123
+ 214,
1124
+ 215,
1125
+ 216,
1126
+ 217,
1127
+ 218,
1128
+ 219,
1129
+ 220,
1130
+ 221,
1131
+ 222,
1132
+ 223,
1133
+ 224,
1134
+ 225,
1135
+ 226,
1136
+ 227,
1137
+ 228,
1138
+ 229,
1139
+ 230,
1140
+ 231,
1141
+ 232,
1142
+ 233,
1143
+ 234,
1144
+ 235,
1145
+ 236,
1146
+ 237,
1147
+ 238,
1148
+ 239,
1149
+ 240,
1150
+ 241,
1151
+ 242,
1152
+ 243,
1153
+ 244,
1154
+ 245,
1155
+ 246,
1156
+ 247,
1157
+ 248,
1158
+ 249,
1159
+ 250,
1160
+ 251,
1161
+ 252,
1162
+ 253,
1163
+ 254,
1164
+ 255,
1165
+ 256,
1166
+ 257,
1167
+ 258,
1168
+ 259,
1169
+ 260,
1170
+ 261,
1171
+ 262
1172
+ ],
1173
+ "shapes": {
1174
+ "prefill_text_embeds_base": [
1175
+ 1,
1176
+ 300,
1177
+ 1024
1178
+ ],
1179
+ "prefill_cos": [
1180
+ 1,
1181
+ 300,
1182
+ 64
1183
+ ],
1184
+ "prefill_sin": [
1185
+ 1,
1186
+ 300,
1187
+ 64
1188
+ ],
1189
+ "decode_cos": [
1190
+ 1,
1191
+ 512,
1192
+ 64
1193
+ ],
1194
+ "decode_sin": [
1195
+ 1,
1196
+ 512,
1197
+ 64
1198
+ ],
1199
+ "token_embedding": [
1200
+ 65425,
1201
+ 1024
1202
+ ],
1203
+ "pixel_values": [
1204
+ 1024,
1205
+ 1536
1206
+ ],
1207
+ "image_embeds": [
1208
+ 256,
1209
+ 1024
1210
+ ]
1211
+ },
1212
+ "dtype": "float16 little-endian raw binaries",
1213
+ "files": {
1214
+ "prefill_text_embeds_base": "prefill_text_embeds_base_fp16.bin",
1215
+ "prefill_cos": "prefill_cos_fp16.bin",
1216
+ "prefill_sin": "prefill_sin_fp16.bin",
1217
+ "decode_cos": "decode_cos_fp16.bin",
1218
+ "decode_sin": "decode_sin_fp16.bin",
1219
+ "token_embedding": "token_embedding_fp16.bin",
1220
+ "canary_pixel_values": "canary_pixel_values_fp16.bin"
1221
+ }
1222
+ }
native_assets/swift_pixel_values_fp16.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:42632cf4f35becf36e42c03f6ad4a78f08e7cdeca8635f9283fe95be82d49b79
3
+ size 3145728
native_assets/token_embedding_fp16.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a00af278f8f6d886c32a9397c7f3a4fea2de987b105131d835d6461131829aba
3
+ size 133990400
scripts/materialize_surya_native_assets.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import importlib.util
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ import torch
9
+ from transformers import AutoProcessor
10
+
11
+
12
+ def load_runtime_module(path: Path):
13
+ spec = importlib.util.spec_from_file_location("surya_coreml_runtime_export", path)
14
+ module = importlib.util.module_from_spec(spec)
15
+ spec.loader.exec_module(module)
16
+ return module
17
+
18
+
19
+ def write_fp16(path: Path, tensor: torch.Tensor) -> None:
20
+ array = tensor.detach().cpu().to(torch.float16).numpy()
21
+ path.parent.mkdir(parents=True, exist_ok=True)
22
+ path.write_bytes(array.tobytes(order="C"))
23
+
24
+
25
+ def main() -> None:
26
+ parser = argparse.ArgumentParser()
27
+ parser.add_argument("--model-id", default="datalab-to/surya-ocr-2")
28
+ parser.add_argument("--runtime-script", type=Path, default=Path(".context/scripts/export_surya_coreml_runtime.py"))
29
+ parser.add_argument("--output-dir", type=Path, default=Path("artifacts/coreml/surya-ocr-2-coreml-8bit/native_assets"))
30
+ parser.add_argument("--max-cache-length", type=int, default=512)
31
+ args = parser.parse_args()
32
+
33
+ rt = load_runtime_module(args.runtime_script)
34
+ output_dir = args.output_dir.expanduser().resolve()
35
+ output_dir.mkdir(parents=True, exist_ok=True)
36
+
37
+ processor = AutoProcessor.from_pretrained(args.model_id, trust_remote_code=True)
38
+ model = rt.load_model(args.model_id, torch.float32)
39
+ sample = rt.build_sample(processor)
40
+
41
+ with torch.no_grad():
42
+ input_ids = sample["input_ids"].to(torch.long)
43
+ attention_mask = sample["attention_mask"].to(torch.long)
44
+ mm_token_type_ids = sample["mm_token_type_ids"].to(torch.long)
45
+ pixel_values = sample["pixel_values"].to(next(model.parameters()).dtype)
46
+ image_grid_thw = sample["image_grid_thw"].to(torch.long)
47
+
48
+ text_embeds = model.model.get_input_embeddings()(input_ids)
49
+ image_outputs = model.model.get_image_features(pixel_values, image_grid_thw, return_dict=True)
50
+ image_embeds = torch.cat(image_outputs.pooler_output, dim=0).to(text_embeds.dtype)
51
+ image_mask, _ = model.model.get_placeholder_mask(input_ids, inputs_embeds=text_embeds, image_features=image_embeds)
52
+ image_token_indices = image_mask.squeeze(0).squeeze(-1).nonzero().flatten().to(torch.long)
53
+ prefill_base = text_embeds.clone()
54
+ prefill_base[:, image_token_indices, :] = 0
55
+
56
+ merged_embeds = text_embeds.clone()
57
+ merged_embeds[:, image_token_indices, :] = image_embeds.reshape(1, image_embeds.shape[0], image_embeds.shape[1])
58
+ position_ids = model.model.compute_3d_position_ids(
59
+ input_ids=input_ids,
60
+ image_grid_thw=image_grid_thw,
61
+ video_grid_thw=None,
62
+ inputs_embeds=merged_embeds,
63
+ attention_mask=attention_mask,
64
+ past_key_values=None,
65
+ mm_token_type_ids=mm_token_type_ids,
66
+ )
67
+ prefill_cos, prefill_sin = model.model.language_model.rotary_emb(merged_embeds, position_ids)
68
+
69
+ one_embed = torch.zeros((1, 1, merged_embeds.shape[-1]), dtype=merged_embeds.dtype)
70
+ step_cos = []
71
+ step_sin = []
72
+ for pos in range(args.max_cache_length):
73
+ step_position_ids = torch.full((3, 1, 1), pos, dtype=torch.long)
74
+ cos, sin = model.model.language_model.rotary_emb(one_embed, step_position_ids)
75
+ step_cos.append(cos)
76
+ step_sin.append(sin)
77
+ step_cos = torch.cat(step_cos, dim=1)
78
+ step_sin = torch.cat(step_sin, dim=1)
79
+
80
+ token_embedding = model.model.get_input_embeddings().weight.detach().clone()
81
+
82
+ write_fp16(output_dir / "prefill_text_embeds_base_fp16.bin", prefill_base)
83
+ write_fp16(output_dir / "prefill_cos_fp16.bin", prefill_cos)
84
+ write_fp16(output_dir / "prefill_sin_fp16.bin", prefill_sin)
85
+ write_fp16(output_dir / "decode_cos_fp16.bin", step_cos)
86
+ write_fp16(output_dir / "decode_sin_fp16.bin", step_sin)
87
+ write_fp16(output_dir / "token_embedding_fp16.bin", token_embedding)
88
+ write_fp16(output_dir / "canary_pixel_values_fp16.bin", pixel_values)
89
+
90
+ constants = {
91
+ "model_id": args.model_id,
92
+ "prompt": rt.PROMPT,
93
+ "input_ids": input_ids.squeeze(0).tolist(),
94
+ "attention_mask": attention_mask.squeeze(0).tolist(),
95
+ "mm_token_type_ids": mm_token_type_ids.squeeze(0).tolist(),
96
+ "image_grid_thw": image_grid_thw.squeeze(0).tolist(),
97
+ "image_token_indices": image_token_indices.tolist(),
98
+ "shapes": {
99
+ "prefill_text_embeds_base": list(prefill_base.shape),
100
+ "prefill_cos": list(prefill_cos.shape),
101
+ "prefill_sin": list(prefill_sin.shape),
102
+ "decode_cos": list(step_cos.shape),
103
+ "decode_sin": list(step_sin.shape),
104
+ "token_embedding": list(token_embedding.shape),
105
+ "pixel_values": list(pixel_values.shape),
106
+ "image_embeds": list(image_embeds.shape),
107
+ },
108
+ "dtype": "float16 little-endian raw binaries",
109
+ "files": {
110
+ "prefill_text_embeds_base": "prefill_text_embeds_base_fp16.bin",
111
+ "prefill_cos": "prefill_cos_fp16.bin",
112
+ "prefill_sin": "prefill_sin_fp16.bin",
113
+ "decode_cos": "decode_cos_fp16.bin",
114
+ "decode_sin": "decode_sin_fp16.bin",
115
+ "token_embedding": "token_embedding_fp16.bin",
116
+ "canary_pixel_values": "canary_pixel_values_fp16.bin",
117
+ },
118
+ }
119
+ (output_dir / "surya_native_constants.json").write_text(json.dumps(constants, indent=2) + "\n", encoding="utf-8")
120
+ vocab = processor.tokenizer.get_vocab()
121
+ id_to_token = [""] * (max(vocab.values()) + 1)
122
+ for token, idx in vocab.items():
123
+ id_to_token[idx] = token
124
+ (output_dir / "id_to_token.json").write_text(json.dumps(id_to_token, ensure_ascii=False) + "\n", encoding="utf-8")
125
+ print(json.dumps({"output_dir": str(output_dir), "files": sorted(p.name for p in output_dir.iterdir())}, indent=2))
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
validation/native_swift_int8_smoke.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "runtime": "SwiftPM SuryaCoreMLRuntime",
3
+ "command": "surya-coreml-smoke --model-dir artifacts/coreml/surya-ocr-2-coreml-8bit --image canary_invoice.png --max-tokens 8 --vision int8",
4
+ "tokens": [1039, 2009, 2046, 2054, 2047, 2041, 2035, 2037, 1979],
5
+ "text": "<p>Invoice ",
6
+ "matches_python_coreml_canary": true,
7
+ "notes": "Native Swift image preprocessing, INT8 vision, FP16 prefill, FP16 decode step, native token embedding lookup, native KV cache update, and native token decoding."
8
+ }