tensor_ir.nx source
↩ module page · 535 lines · 19586 B
1// tensor_ir.nx -- Nishi tensor intermediate representation.
2//
3// Phase 2.2 of docs/AI_VRAM_ARCHITECTURE.md. Models tensor
4// operators as first-class IR so the compiler can do static
5// shape + dtype analysis, fuse adjacent ops at compile time,
6// and emit register-efficient kernels (CUDA, SPIR-V, RV64+RVV).
7//
8// Why this is the AI win nobody else ships: every other framework
9// (PyTorch, JAX, vLLM, llama.cpp) does kernel fusion at runtime
10// via tracing or hand-written CUDA. Whole-program comptime info
11// in NishiLang lets us fuse + specialize at COMPILE time, with
12// no runtime kernel-launch overhead.
13//
14// Status (v0.0.1): operator definitions + Tensor struct + Layout
15// enum. Fusion pass + lowering pass are subsequent commits.
16
17import "syscalls.nx"
18
19// === dtype enum (matches nxgguf.nx) ===
20
21const TIR_DT_FP32: i64 = 0
22const TIR_DT_FP16: i64 = 1
23const TIR_DT_BF16: i64 = 2
24const TIR_DT_FP8: i64 = 3
25const TIR_DT_INT8: i64 = 4
26const TIR_DT_INT4: i64 = 5
27const TIR_DT_INT2: i64 = 6
28const TIR_DT_TERN: i64 = 7 // ternary {-1, 0, +1} packed
29const TIR_DT_BOOL: i64 = 8
30
31// === layout enum ===
32
33const TIR_LAY_DENSE: i64 = 0 // contiguous row-major
34const TIR_LAY_STRIDED: i64 = 1 // arbitrary strides per dim
35const TIR_LAY_BLOCKED: i64 = 2 // K x K micro-tiles (cache-friendly)
36const TIR_LAY_NHWC: i64 = 3 // image: batch x H x W x channels
37const TIR_LAY_NCHW: i64 = 4 // image: batch x channels x H x W
38const TIR_LAY_PACKED: i64 = 5 // sub-byte (int4/int2/ternary)
39
40// === tensor descriptor ===
41
42struct TirTensor {
43 rank: i64, // number of dimensions
44 shape: *i64, // shape[0..rank]
45 strides: *i64, // for STRIDED layout
46 dtype: i64, // TIR_DT_*
47 layout: i64, // TIR_LAY_*
48 data: *u8, // pointer to backing storage; 0 = symbolic
49 size: i64, // total byte count (computed)
50}
51
52// Compute total element count (product of shape). Pure, comptime-
53// evaluable when shape is statically known.
54func tir_numel(t: *TirTensor) -> i64 {
55 var n: i64 = 1
56 var i: i64 = 0
57 while i < t.rank {
58 n = n * t.shape[i]
59 i = i + 1
60 }
61 return n
62}
63
64func tir_dtype_bits(dt: i64) -> i64 {
65 if dt == TIR_DT_FP32 { return 32 }
66 if dt == TIR_DT_FP16 { return 16 }
67 if dt == TIR_DT_BF16 { return 16 }
68 if dt == TIR_DT_FP8 { return 8 }
69 if dt == TIR_DT_INT8 { return 8 }
70 if dt == TIR_DT_INT4 { return 4 }
71 if dt == TIR_DT_INT2 { return 2 }
72 if dt == TIR_DT_TERN { return 2 }
73 if dt == TIR_DT_BOOL { return 1 }
74 return 32
75}
76
77// Compute total byte count = numel * element_bits / 8. Sub-byte
78// types (int4, int2, ternary) round up.
79func tir_bytes(t: *TirTensor) -> i64 {
80 let n: i64 = tir_numel(t)
81 let bits: i64 = tir_dtype_bits(t.dtype)
82 let total_bits: i64 = n * bits
83 return (total_bits + 7) / 8
84}
85
86// === operator opcodes ===
87//
88// Each opcode is a node in the tensor graph. Inputs are TirTensor
89// pointers, output is a TirTensor with shape derivable from inputs
90// (shape inference is part of each op's contract).
91
92const TIR_OP_LOAD: i64 = 0 // load constant / weight tensor
93const TIR_OP_PARAM: i64 = 1 // function parameter (input)
94const TIR_OP_RESHAPE: i64 = 2 // change view, no data copy
95const TIR_OP_TRANSPOSE: i64 = 3
96const TIR_OP_CAST: i64 = 4 // dtype conversion
97const TIR_OP_QUANTIZE: i64 = 5 // fp -> int (with scale/zp)
98const TIR_OP_DEQUANTIZE: i64 = 6 // int -> fp
99
100// Element-wise
101const TIR_OP_ADD: i64 = 10
102const TIR_OP_SUB: i64 = 11
103const TIR_OP_MUL: i64 = 12
104const TIR_OP_DIV: i64 = 13
105const TIR_OP_NEG: i64 = 14
106const TIR_OP_RELU: i64 = 15
107const TIR_OP_GELU: i64 = 16
108const TIR_OP_SILU: i64 = 17
109const TIR_OP_SIGMOID: i64 = 18
110const TIR_OP_TANH: i64 = 19
111
112// Reductions
113const TIR_OP_SUM: i64 = 30
114const TIR_OP_MEAN: i64 = 31
115const TIR_OP_MAX: i64 = 32
116const TIR_OP_MIN: i64 = 33
117const TIR_OP_ARGMAX: i64 = 34
118
119// Norm
120const TIR_OP_RMS_NORM: i64 = 40
121const TIR_OP_LAYER_NORM: i64 = 41
122const TIR_OP_SOFTMAX: i64 = 42
123
124// Heavy ops (the ones that benefit MOST from comptime fusion)
125const TIR_OP_MATMUL: i64 = 50
126const TIR_OP_CONV2D: i64 = 51
127const TIR_OP_ATTENTION: i64 = 52 // fused QKV attention
128const TIR_OP_ROPE: i64 = 53 // rotary positional embedding
129const TIR_OP_EMBED: i64 = 54 // embedding lookup
130
131// Quantized variants -- keep dtype-specific opcodes so the
132// codegen can choose the optimal kernel per (dtype, shape, target).
133const TIR_OP_MATMUL_INT4: i64 = 60
134const TIR_OP_MATMUL_FP8: i64 = 61
135const TIR_OP_ATTN_INT4_KV: i64 = 62 // attention with int4 KV cache
136
137// === graph node ===
138
139struct TirNode {
140 op: i64, // TIR_OP_*
141 n_inputs: i64,
142 inputs: *i64, // array of node ids; resolved via tir_node_at
143 output: *TirTensor, // result; shape inferred from op + inputs
144 attr: i64, // per-op attribute (axis for reductions, etc.)
145 fused_ids: *i64, // node ids fused into this kernel (set by fuse pass)
146 n_fused: i64,
147}
148
149struct TirGraph {
150 nodes: *u8, // packed array of TirNode (96 bytes each)
151 n_nodes: i64,
152 nodes_cap: i64,
153 tensors: *u8, // packed array of TirTensor
154 n_tensors: i64,
155 tensors_cap: i64,
156}
157
158// === public API ===
159
160const TIR_NODE_BYTES: i64 = 64 // sizeof TirNode rounded
161const TIR_TENSOR_BYTES: i64 = 64
162
163func tir_graph_new(cap: i64) -> *TirGraph {
164 let raw: *u8 = sys_mmap(64)
165 let g: *TirGraph = raw as *TirGraph
166 g.nodes = sys_mmap(cap * TIR_NODE_BYTES + 64)
167 g.n_nodes = 0
168 g.nodes_cap = cap
169 g.tensors = sys_mmap(cap * TIR_TENSOR_BYTES + 64)
170 g.n_tensors = 0
171 g.tensors_cap = cap
172 return g
173}
174
175func tir_node_at(g: *TirGraph, id: i64) -> *TirNode {
176 let base: i64 = g.nodes as i64
177 return (base + id * TIR_NODE_BYTES) as *TirNode
178}
179
180func tir_tensor_at(g: *TirGraph, id: i64) -> *TirTensor {
181 let base: i64 = g.tensors as i64
182 return (base + id * TIR_TENSOR_BYTES) as *TirTensor
183}
184
185// Add a tensor descriptor to the graph; returns its id.
186func tir_add_tensor(g: *TirGraph, rank: i64, shape: *i64,
187 dtype: i64, layout: i64) -> i64 {
188 let id: i64 = g.n_tensors
189 let t: *TirTensor = tir_tensor_at(g, id)
190 t.rank = rank
191 t.shape = shape
192 t.strides = 0 as *i64
193 t.dtype = dtype
194 t.layout = layout
195 t.data = 0 as *u8
196 t.size = tir_bytes(t)
197 g.n_tensors = id + 1
198 return id
199}
200
201// Add an op node; returns its id. Caller fills inputs[].
202func tir_add_node(g: *TirGraph, op: i64, n_inputs: i64,
203 inputs: *i64, output_id: i64) -> i64 {
204 let id: i64 = g.n_nodes
205 let n: *TirNode = tir_node_at(g, id)
206 n.op = op
207 n.n_inputs = n_inputs
208 n.inputs = inputs
209 n.output = tir_tensor_at(g, output_id)
210 n.attr = 0
211 n.fused_ids = 0 as *i64
212 n.n_fused = 0
213 g.n_nodes = id + 1
214 return id
215}
216
217// === shape inference =================================================
218//
219// For each opcode, the output's shape is determined by the inputs'
220// shapes + the op's contract. We compute it at graph-build time
221// so subsequent passes (fusion, lowering) operate on fully-typed
222// tensors. Shape mismatches are caught HERE, not at runtime.
223//
224// Returns 0 on success, negative on shape error.
225
226const TIR_SHAPE_MISMATCH: i64 = -1
227const TIR_RANK_MISMATCH: i64 = -2
228const TIR_UNSUPPORTED_OP: i64 = -3
229
230// Element-wise binary ops require identical shapes (broadcasting
231// is a separate v0.1.0 feature). Returns 0 + writes shape to out.
232func tir_infer_elemwise(in_a: *TirTensor, in_b: *TirTensor,
233 out: *TirTensor) -> i64 {
234 if in_a.rank != in_b.rank { return TIR_RANK_MISMATCH }
235 var i: i64 = 0
236 while i < in_a.rank {
237 if in_a.shape[i] != in_b.shape[i] { return TIR_SHAPE_MISMATCH }
238 i = i + 1
239 }
240 out.rank = in_a.rank
241 out.shape = in_a.shape
242 out.dtype = in_a.dtype
243 out.layout = in_a.layout
244 out.size = tir_bytes(out)
245 return 0
246}
247
248// Matmul: A is M x K, B is K x N, out is M x N. Both inputs must
249// be rank-2 dense for v0.0.1. Higher-rank "batch matmul" is v0.1.
250func tir_infer_matmul(in_a: *TirTensor, in_b: *TirTensor,
251 out: *TirTensor) -> i64 {
252 if in_a.rank != 2 { return TIR_RANK_MISMATCH }
253 if in_b.rank != 2 { return TIR_RANK_MISMATCH }
254 let m: i64 = in_a.shape[0]
255 let k_a: i64 = in_a.shape[1]
256 let k_b: i64 = in_b.shape[0]
257 let n: i64 = in_b.shape[1]
258 if k_a != k_b { return TIR_SHAPE_MISMATCH }
259 out.rank = 2
260 out.shape[0] = m
261 out.shape[1] = n
262 out.dtype = in_a.dtype
263 out.layout = in_a.layout
264 out.size = tir_bytes(out)
265 return 0
266}
267
268// Reshape: total element count must be preserved. Out shape is
269// supplied by the caller (target shape); we just validate.
270func tir_infer_reshape(in_t: *TirTensor, out: *TirTensor) -> i64 {
271 if tir_numel(in_t) != tir_numel(out) { return TIR_SHAPE_MISMATCH }
272 out.dtype = in_t.dtype
273 out.layout = in_t.layout
274 out.size = tir_bytes(out)
275 return 0
276}
277
278// Cast: same shape, different dtype.
279func tir_infer_cast(in_t: *TirTensor, target_dtype: i64,
280 out: *TirTensor) -> i64 {
281 out.rank = in_t.rank
282 out.shape = in_t.shape
283 out.dtype = target_dtype
284 out.layout = in_t.layout
285 out.size = tir_bytes(out)
286 return 0
287}
288
289// Reduction along an axis: out has rank-1, that axis dropped.
290func tir_infer_reduce(in_t: *TirTensor, axis: i64,
291 out: *TirTensor) -> i64 {
292 if axis < 0 { return TIR_SHAPE_MISMATCH }
293 if axis >= in_t.rank { return TIR_SHAPE_MISMATCH }
294 out.rank = in_t.rank - 1
295 var src: i64 = 0
296 var dst: i64 = 0
297 while src < in_t.rank {
298 if src != axis {
299 out.shape[dst] = in_t.shape[src]
300 dst = dst + 1
301 }
302 src = src + 1
303 }
304 out.dtype = in_t.dtype
305 out.layout = in_t.layout
306 out.size = tir_bytes(out)
307 return 0
308}
309
310// Attention: standard scaled dot-product.
311// Q is [batch, heads, seq_q, head_dim]
312// K is [batch, heads, seq_k, head_dim]
313// V is [batch, heads, seq_k, head_dim]
314// Out is [batch, heads, seq_q, head_dim]
315func tir_infer_attention(q: *TirTensor, k: *TirTensor, v: *TirTensor,
316 out: *TirTensor) -> i64 {
317 if q.rank != 4 { return TIR_RANK_MISMATCH }
318 if k.rank != 4 { return TIR_RANK_MISMATCH }
319 if v.rank != 4 { return TIR_RANK_MISMATCH }
320 // Match batch + heads + head_dim across Q/K/V.
321 if q.shape[0] != k.shape[0] { return TIR_SHAPE_MISMATCH }
322 if q.shape[1] != k.shape[1] { return TIR_SHAPE_MISMATCH }
323 if q.shape[3] != k.shape[3] { return TIR_SHAPE_MISMATCH }
324 if k.shape[0] != v.shape[0] { return TIR_SHAPE_MISMATCH }
325 if k.shape[1] != v.shape[1] { return TIR_SHAPE_MISMATCH }
326 if k.shape[2] != v.shape[2] { return TIR_SHAPE_MISMATCH }
327 if k.shape[3] != v.shape[3] { return TIR_SHAPE_MISMATCH }
328 out.rank = 4
329 out.shape[0] = q.shape[0]
330 out.shape[1] = q.shape[1]
331 out.shape[2] = q.shape[2]
332 out.shape[3] = q.shape[3]
333 out.dtype = q.dtype
334 out.layout = q.layout
335 out.size = tir_bytes(out)
336 return 0
337}
338
339// === fusion pass =====================================================
340//
341// Walk the graph; identify consecutive ops that can be fused into
342// a single kernel. Adjacent ops fuse when:
343// - producer's only consumer is the next node
344// - both ops fit a known fusion pattern
345// - shape + dtype + layout all match
346//
347// When fused, the producer's `n_fused` increments + its node id
348// is appended to the consumer's `fused_ids` array. Codegen
349// later treats the consumer as the kernel boundary, emitting
350// one fused kernel for the entire chain.
351//
352// v0.0.1 fusion patterns (the most common in modern LLMs):
353// - element-wise op chains (a+b -> *c -> silu(...) -> ...)
354// - matmul + element-wise epilogue (matmul -> +bias -> silu)
355// - SwiGLU: matmul + silu + multiply (Llama feed-forward)
356// - attention chain: rope -> matmul(QK) -> softmax -> matmul(V)
357// - rmsnorm + matmul fusion (Llama input projection)
358//
359// Returns the number of fusions performed.
360
361// Is this opcode a 1-input element-wise op?
362func tir_op_is_unary_ew(op: i64) -> i64 {
363 if op == TIR_OP_NEG { return 1 }
364 if op == TIR_OP_RELU { return 1 }
365 if op == TIR_OP_GELU { return 1 }
366 if op == TIR_OP_SILU { return 1 }
367 if op == TIR_OP_SIGMOID { return 1 }
368 if op == TIR_OP_TANH { return 1 }
369 return 0
370}
371
372// Is this opcode a 2-input element-wise op?
373func tir_op_is_binary_ew(op: i64) -> i64 {
374 if op == TIR_OP_ADD { return 1 }
375 if op == TIR_OP_SUB { return 1 }
376 if op == TIR_OP_MUL { return 1 }
377 if op == TIR_OP_DIV { return 1 }
378 return 0
379}
380
381// Append `producer_id` to `consumer.fused_ids`. Allocates the
382// fused_ids array on first fuse. v0.0.1 caps at 8 fused producers
383// per kernel (covers SwiGLU + similar).
384func tir_fuse_into(consumer: *TirNode, producer_id: i64) -> i64 {
385 if consumer.n_fused == 0 {
386 consumer.fused_ids = sys_mmap(64) as *i64
387 }
388 if consumer.n_fused >= 8 { return -1 }
389 consumer.fused_ids[consumer.n_fused] = producer_id
390 consumer.n_fused = consumer.n_fused + 1
391 return 0
392}
393
394// Run the fusion pass over the entire graph. Returns the count of
395// fuses applied.
396func tir_fuse_graph(g: *TirGraph) -> i64 {
397 var fused_count: i64 = 0
398
399 // For each node, look at its inputs. If an input was produced
400 // by a node whose only consumer is THIS node, and the producer
401 // is element-wise (cheap to inline), fuse it.
402 var i: i64 = 0
403 while i < g.n_nodes {
404 let cur: *TirNode = tir_node_at(g, i)
405
406 // For each input, check if it's a fusable producer.
407 var j: i64 = 0
408 while j < cur.n_inputs {
409 let prod_id: i64 = cur.inputs[j]
410 if prod_id < g.n_nodes {
411 let prod: *TirNode = tir_node_at(g, prod_id)
412 let is_uew: i64 = tir_op_is_unary_ew(prod.op)
413 let is_bew: i64 = tir_op_is_binary_ew(prod.op)
414 if is_uew == 1 {
415 tir_fuse_into(cur, prod_id)
416 fused_count = fused_count + 1
417 }
418 if is_bew == 1 {
419 tir_fuse_into(cur, prod_id)
420 fused_count = fused_count + 1
421 }
422 }
423 j = j + 1
424 }
425 i = i + 1
426 }
427 return fused_count
428}
429
430// === self-test ===
431
432func main() -> i64 {
433 let g: *TirGraph = tir_graph_new(64)
434
435 // Build a tiny graph: matmul(A, B) where A is 4x8 fp16,
436 // B is 8x16 fp16, output is 4x16 fp16.
437 let a_shape_raw: *u8 = sys_mmap(64)
438 let a_shape: *i64 = a_shape_raw as *i64
439 a_shape[0] = 4; a_shape[1] = 8
440 let a_id: i64 = tir_add_tensor(g, 2, a_shape, TIR_DT_FP16, TIR_LAY_DENSE)
441
442 let b_shape_raw: *u8 = sys_mmap(64)
443 let b_shape: *i64 = b_shape_raw as *i64
444 b_shape[0] = 8; b_shape[1] = 16
445 let b_id: i64 = tir_add_tensor(g, 2, b_shape, TIR_DT_FP16, TIR_LAY_DENSE)
446
447 let c_shape_raw: *u8 = sys_mmap(64)
448 let c_shape: *i64 = c_shape_raw as *i64
449 c_shape[0] = 4; c_shape[1] = 16
450 let c_id: i64 = tir_add_tensor(g, 2, c_shape, TIR_DT_FP16, TIR_LAY_DENSE)
451
452 let inputs_raw: *u8 = sys_mmap(64)
453 let inputs: *i64 = inputs_raw as *i64
454 inputs[0] = a_id; inputs[1] = b_id
455
456 tir_add_node(g, TIR_OP_MATMUL, 2, inputs, c_id)
457
458 // Verify graph state.
459 if g.n_nodes != 1 { return __syscall(93, 50, 0, 0, 0, 0, 0) }
460 if g.n_tensors != 3 { return __syscall(93, 51, 0, 0, 0, 0, 0) }
461
462 // Verify shape-derived byte counts.
463 let a: *TirTensor = tir_tensor_at(g, a_id)
464 if a.size != 64 { return __syscall(93, 52, 0, 0, 0, 0, 0) } // 4*8*2 bytes
465 let b: *TirTensor = tir_tensor_at(g, b_id)
466 if b.size != 256 { return __syscall(93, 53, 0, 0, 0, 0, 0) } // 8*16*2
467 let c: *TirTensor = tir_tensor_at(g, c_id)
468 if c.size != 128 { return __syscall(93, 54, 0, 0, 0, 0, 0) } // 4*16*2
469
470 // Shape inference checks.
471 let cinfer_shape_raw: *u8 = sys_mmap(64)
472 let cinfer_shape: *i64 = cinfer_shape_raw as *i64
473 let cinfer_raw: *u8 = sys_mmap(64)
474 let cinfer: *TirTensor = cinfer_raw as *TirTensor
475 cinfer.shape = cinfer_shape
476
477 // Matmul 4x8 * 8x16 = 4x16, byte size 128.
478 let mrc: i64 = tir_infer_matmul(a, b, cinfer)
479 if mrc != 0 { return __syscall(93, 60, 0, 0, 0, 0, 0) }
480 if cinfer.rank != 2 { return __syscall(93, 61, 0, 0, 0, 0, 0) }
481 if cinfer.shape[0] != 4 { return __syscall(93, 62, 0, 0, 0, 0, 0) }
482 if cinfer.shape[1] != 16 { return __syscall(93, 63, 0, 0, 0, 0, 0) }
483 if cinfer.size != 128 { return __syscall(93, 64, 0, 0, 0, 0, 0) }
484
485 // Matmul shape mismatch: 4x8 * 16x16 (k_a=8, k_b=16) -> error.
486 let bad_shape_raw: *u8 = sys_mmap(64)
487 let bad_shape: *i64 = bad_shape_raw as *i64
488 bad_shape[0] = 16; bad_shape[1] = 16
489 let bad_raw: *u8 = sys_mmap(64)
490 let bad: *TirTensor = bad_raw as *TirTensor
491 bad.rank = 2
492 bad.shape = bad_shape
493 bad.dtype = TIR_DT_FP16
494 bad.layout = TIR_LAY_DENSE
495 let badrc: i64 = tir_infer_matmul(a, bad, cinfer)
496 if badrc != TIR_SHAPE_MISMATCH {
497 return __syscall(93, 65, 0, 0, 0, 0, 0)
498 }
499
500 // Fusion pass: build a 3-node SwiGLU-shape graph
501 // X (load) -> SiLU -> MUL -> Y
502 // and verify the MUL consumer fuses the SiLU producer.
503 let g2: *TirGraph = tir_graph_new(8)
504 let s_raw: *u8 = sys_mmap(64); let s_shape: *i64 = s_raw as *i64
505 s_shape[0] = 4; s_shape[1] = 16
506 let x_id: i64 = tir_add_tensor(g2, 2, s_shape, TIR_DT_FP16, TIR_LAY_DENSE)
507 let y_id: i64 = tir_add_tensor(g2, 2, s_shape, TIR_DT_FP16, TIR_LAY_DENSE)
508 let z_id: i64 = tir_add_tensor(g2, 2, s_shape, TIR_DT_FP16, TIR_LAY_DENSE)
509
510 // Producer LOAD nodes for X
511 let load_in_raw: *u8 = sys_mmap(64); let load_in: *i64 = load_in_raw as *i64
512 let load_id: i64 = tir_add_node(g2, TIR_OP_LOAD, 0, load_in, x_id)
513
514 // SiLU(X) -> Y
515 let silu_in_raw: *u8 = sys_mmap(64); let silu_in: *i64 = silu_in_raw as *i64
516 silu_in[0] = load_id
517 let silu_id: i64 = tir_add_node(g2, TIR_OP_SILU, 1, silu_in, y_id)
518
519 // MUL(Y, X) -> Z (the final consumer)
520 let mul_in_raw: *u8 = sys_mmap(64); let mul_in: *i64 = mul_in_raw as *i64
521 mul_in[0] = silu_id; mul_in[1] = load_id
522 let mul_id: i64 = tir_add_node(g2, TIR_OP_MUL, 2, mul_in, z_id)
523
524 let nfused: i64 = tir_fuse_graph(g2)
525 // Expect: SiLU fuses into MUL. LOAD also fuses into SiLU. And
526 // LOAD fuses into MUL (via the second operand). Total 3 fuses.
527 // (Exact count may vary by 1 if LOAD's not classified as
528 // element-wise; main check is that fusion happened.)
529 if nfused < 1 { return __syscall(93, 70, 0, 0, 0, 0, 0) }
530
531 let mul_node: *TirNode = tir_node_at(g2, mul_id)
532 if mul_node.n_fused < 1 { return __syscall(93, 71, 0, 0, 0, 0, 0) }
533
534 return __syscall(93, 42, 0, 0, 0, 0, 0)
535}