oxicuda-ptx 0.4.1

OxiCUDA PTX - PTX code generation DSL and IR for GPU kernel development
Documentation
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
//! Additional tests for the PTX validator — SM compatibility and register
//! pressure checks introduced in the second implementation pass.

use std::fmt::Write as _;

use super::{
    IrErrorKind, ValidationError, validate_branch_labels, validate_ir_instructions, validate_ptx,
    validate_ptx_for_target,
};
use crate::arch::SmVersion;
use crate::ir::{Instruction, Operand, PtxType, Register};

// ---------------------------------------------------------------------------
// SM compatibility tests
// ---------------------------------------------------------------------------

#[test]
fn test_valid_ptx_passes_validation() {
    let ptx = ".version 7.0\n.target sm_80\n.address_size 64\n\
               .visible .entry test_kernel(.param .u32 %param_n) {\n\
               .reg .u32 %r<4>;\n\
               mov.u32 %r0, %tid.x;\n\
               ret;\n\
               }\n";
    let result = validate_ptx_for_target(ptx, SmVersion::Sm80);
    assert!(
        result.is_ok(),
        "expected no errors for valid sm_80 PTX, got: {:?}",
        result.errors
    );
}

#[test]
fn test_cp_async_requires_sm80() {
    // cp.async on sm_75 should trigger SmIncompatibleInstruction
    let ptx = ".version 6.4\n.target sm_75\n.address_size 64\n\
               .visible .entry k() {\n\
               cp.async.ca.shared.global [%r0], [%rd0], 4;\n\
               ret;\n\
               }\n";
    let result = validate_ptx_for_target(ptx, SmVersion::Sm75);
    let compat_errors: Vec<_> = result
        .errors
        .iter()
        .filter(|e| matches!(e, ValidationError::SmIncompatibleInstruction { .. }))
        .collect();
    assert!(
        !compat_errors.is_empty(),
        "expected SmIncompatibleInstruction for cp.async on sm_75"
    );
    // Verify the error names the required SM correctly
    let msg = format!("{}", compat_errors[0]);
    assert!(
        msg.contains("sm_80"),
        "expected required_sm=sm_80, got: {msg}"
    );
}

#[test]
fn test_wgmma_requires_sm90() {
    let ptx = ".version 7.0\n.target sm_80\n.address_size 64\n\
               .visible .entry k() {\n\
               wgmma.mma_async.sync.aligned.m64n128k16.f32.f16.f16 {...};\n\
               ret;\n\
               }\n";
    let result = validate_ptx_for_target(ptx, SmVersion::Sm80);
    let compat_errors: Vec<_> = result
        .errors
        .iter()
        .filter(|e| matches!(e, ValidationError::SmIncompatibleInstruction { .. }))
        .collect();
    assert!(
        !compat_errors.is_empty(),
        "expected SmIncompatibleInstruction for wgmma on sm_80"
    );
    let msg = format!("{}", compat_errors[0]);
    assert!(
        msg.contains("sm_90"),
        "expected required_sm=sm_90, got: {msg}"
    );
}

#[test]
fn test_fp8_requires_sm89() {
    // Using .e4m3 type in an sm_80 kernel
    let ptx = ".version 7.0\n.target sm_80\n.address_size 64\n\
               .visible .entry k() {\n\
               cvt.rn.e4m3x2.f32 %r0, %f0, %f1;\n\
               ret;\n\
               }\n";
    let result = validate_ptx_for_target(ptx, SmVersion::Sm80);
    let compat_errors: Vec<_> = result
        .errors
        .iter()
        .filter(|e| matches!(e, ValidationError::SmIncompatibleInstruction { .. }))
        .collect();
    assert!(
        !compat_errors.is_empty(),
        "expected SmIncompatibleInstruction for fp8 e4m3 on sm_80"
    );
    let msg = format!("{}", compat_errors[0]);
    assert!(
        msg.contains("sm_89"),
        "expected required_sm=sm_89, got: {msg}"
    );
}

#[test]
fn test_too_many_registers_error() {
    // Build a PTX with more than 255 distinct registers (%f0..%f259 = 260 distinct)
    let mut ptx_body = String::from(
        ".version 7.0\n.target sm_80\n.address_size 64\n\
         .visible .entry k() {\n\
         .reg .f32 %f<260>;\n",
    );
    for i in 0..260_usize {
        let _ = writeln!(ptx_body, "    mov.f32 %f{i}, 0f00000000;");
    }
    ptx_body.push_str("    ret;\n}\n");

    let result = validate_ptx_for_target(&ptx_body, SmVersion::Sm80);
    // More than 255 DISTINCT VIRTUAL registers is valid PTX — ptxas spills the
    // excess to local memory. It must be reported as a warning, never a hard
    // error (255 is a post-allocation *physical* limit, not a source limit).
    assert!(
        !result
            .errors
            .iter()
            .any(|e| matches!(e, ValidationError::RegisterPressureExceeded { .. })),
        "excess virtual registers must not be a hard error, errors: {:?}",
        result.errors
    );
    assert!(
        result.warnings.iter().any(|w| w.contains("260")),
        "expected a register-pressure warning for 260 virtual registers, warnings: {:?}",
        result.warnings
    );
}

#[test]
fn test_unknown_target_warns() {
    // A `.target` we cannot recognize disables SM/shared-mem checks; that loss
    // of coverage must surface as a warning rather than pass silently.
    let ptx = ".version 9.9\n.target sm_999\n.address_size 64\n\
               .visible .entry k() { ret; }\n";
    let result = validate_ptx(ptx);
    assert!(
        result.warnings.iter().any(|w| w.contains("sm_999")),
        "expected an unrecognized-target warning, warnings: {:?}",
        result.warnings
    );
    // A recognized target must NOT produce that warning.
    let good = ".version 8.5\n.target sm_80\n.address_size 64\n\
                .visible .entry k() { ret; }\n";
    let good_result = validate_ptx(good);
    assert!(
        !good_result
            .warnings
            .iter()
            .any(|w| w.contains("unrecognized .target")),
        "recognized target should not warn, warnings: {:?}",
        good_result.warnings
    );
}

#[test]
fn test_shared_memory_over_limit_error() {
    // sm_75 allows 65536 bytes; declare 70000 bytes
    let ptx = ".version 6.4\n.target sm_75\n.address_size 64\n\
               .visible .entry k() {\n\
               .shared .align 4 .b8 smem[70000];\n\
               ret;\n\
               }\n";
    let result = validate_ptx_for_target(ptx, SmVersion::Sm75);
    assert!(
        result.has_errors(),
        "expected error for shared memory exceeding sm_75 limit"
    );
    assert!(
        result.errors.iter().any(|e| matches!(
            e,
            ValidationError::InvalidSharedMemSize {
                declared: 70000,
                ..
            }
        )),
        "expected InvalidSharedMemSize error, got: {:?}",
        result.errors
    );
}

#[test]
fn test_wgmma_valid_on_sm90() {
    // wgmma on sm_90 should pass the SM compatibility check
    let ptx = ".version 8.0\n.target sm_90\n.address_size 64\n\
               .visible .entry k() {\n\
               wgmma.mma_async.sync.aligned.m64n128k16.f32.f16.f16 {...};\n\
               ret;\n\
               }\n";
    let result = validate_ptx_for_target(ptx, SmVersion::Sm90);
    // Should not have any SM-compatibility errors
    let compat_errors: Vec<_> = result
        .errors
        .iter()
        .filter(|e| matches!(e, ValidationError::SmIncompatibleInstruction { .. }))
        .collect();
    assert!(
        compat_errors.is_empty(),
        "wgmma should be valid on sm_90, got errors: {compat_errors:?}"
    );
}

#[test]
fn test_empty_ptx_valid() {
    // Empty PTX should report missing directives but no SM/register errors
    let result = validate_ptx("");
    // Should have MissingVersionDirective and MissingTargetDirective errors
    assert!(result.has_errors());
    assert!(
        result
            .errors
            .iter()
            .any(|e| matches!(e, ValidationError::MissingVersionDirective))
    );
    assert!(
        result
            .errors
            .iter()
            .any(|e| matches!(e, ValidationError::MissingTargetDirective))
    );
    // No SM or register pressure errors (nothing to scan)
    assert!(!result.errors.iter().any(|e| matches!(
        e,
        ValidationError::SmIncompatibleInstruction { .. }
            | ValidationError::RegisterPressureExceeded { .. }
    )));
}

#[test]
fn test_sm_compat_error_display() {
    let err = ValidationError::SmIncompatibleInstruction {
        instruction: "cp.async".to_string(),
        required_sm: "sm_80".to_string(),
        found_sm: "sm_75".to_string(),
    };
    let msg = format!("{err}");
    assert!(msg.contains("cp.async"));
    assert!(msg.contains("sm_80"));
    assert!(msg.contains("sm_75"));
}

#[test]
fn test_register_pressure_exceeded_display() {
    let err = ValidationError::RegisterPressureExceeded {
        count: 300,
        max_allowed: 255,
    };
    let msg = format!("{err}");
    assert!(msg.contains("300"));
    assert!(msg.contains("255"));
}

#[test]
fn test_mma_sync_requires_sm75() {
    // mma.sync is available on sm_75+ — verify sm_75 target passes.
    let ptx = ".version 6.4\n.target sm_75\n.address_size 64\n\
               .visible .entry k() {\n\
               mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32 {...};\n\
               ret;\n\
               }\n";
    let result = validate_ptx_for_target(ptx, SmVersion::Sm75);
    let compat_errors: Vec<_> = result
        .errors
        .iter()
        .filter(|e| {
            matches!(e, ValidationError::SmIncompatibleInstruction { instruction, .. }
                if instruction.contains("mma.sync"))
        })
        .collect();
    // sm_75 supports mma.sync, so no error expected
    assert!(
        compat_errors.is_empty(),
        "mma.sync should be valid on sm_75: {compat_errors:?}"
    );
}

// ---------------------------------------------------------------------------
// Branch / label validation tests (F085)
// ---------------------------------------------------------------------------

fn reg(name: &str, ty: PtxType) -> Register {
    Register {
        name: name.to_string(),
        ty,
    }
}

#[test]
fn test_branch_to_undefined_label_errors() {
    let instructions = vec![
        Instruction::Branch {
            target: "nowhere".to_string(),
            predicate: None,
        },
        Instruction::Label("elsewhere".to_string()),
        Instruction::Return,
    ];
    let result = validate_ir_instructions(&instructions);
    assert!(
        result
            .errors
            .iter()
            .any(|e| e.kind == IrErrorKind::InvalidOperand && e.message.contains("nowhere")),
        "expected an undefined-branch-target error, got: {:?}",
        result.errors
    );
}

#[test]
fn test_branch_to_defined_label_ok() {
    let instructions = vec![
        Instruction::Branch {
            target: "done".to_string(),
            predicate: None,
        },
        Instruction::Label("done".to_string()),
        Instruction::Return,
    ];
    let result = validate_branch_labels(&instructions);
    assert!(
        result.errors.is_empty(),
        "forward branch to a defined label must be valid, got: {:?}",
        result.errors
    );
}

#[test]
fn test_duplicate_label_errors() {
    let instructions = vec![
        Instruction::Label("loop".to_string()),
        Instruction::Return,
        Instruction::Label("loop".to_string()),
    ];
    let result = validate_branch_labels(&instructions);
    assert!(
        result
            .errors
            .iter()
            .any(|e| e.kind == IrErrorKind::InvalidOperand && e.message.contains("duplicate")),
        "expected a duplicate-label error, got: {:?}",
        result.errors
    );
}

// ---------------------------------------------------------------------------
// ldmatrix operand consistency (F106)
// ---------------------------------------------------------------------------

#[test]
fn test_ldmatrix_fragment_count_mismatch_errors() {
    // num_fragments = 4 but only 2 destination registers supplied.
    let instructions = vec![Instruction::Ldmatrix {
        num_fragments: 4,
        trans: false,
        dst_regs: vec![reg("%r0", PtxType::B32), reg("%r1", PtxType::B32)],
        src_addr: Operand::Register(reg("%rd0", PtxType::U64)),
    }];
    let result = validate_ir_instructions(&instructions);
    assert!(
        result
            .errors
            .iter()
            .any(|e| e.kind == IrErrorKind::InvalidOperand && e.message.contains("ldmatrix")),
        "expected an ldmatrix fragment-count mismatch error, got: {:?}",
        result.errors
    );
}

#[test]
fn test_ldmatrix_invalid_fragment_count_errors() {
    // num_fragments = 3 is not a legal ldmatrix width.
    let instructions = vec![Instruction::Ldmatrix {
        num_fragments: 3,
        trans: false,
        dst_regs: vec![
            reg("%r0", PtxType::B32),
            reg("%r1", PtxType::B32),
            reg("%r2", PtxType::B32),
        ],
        src_addr: Operand::Register(reg("%rd0", PtxType::U64)),
    }];
    let result = validate_ir_instructions(&instructions);
    assert!(
        result
            .errors
            .iter()
            .any(|e| e.kind == IrErrorKind::InvalidOperand && e.message.contains("1, 2, or 4")),
        "expected an ldmatrix invalid-width error, got: {:?}",
        result.errors
    );
}

#[test]
fn test_ldmatrix_valid_x4_ok() {
    let instructions = vec![Instruction::Ldmatrix {
        num_fragments: 4,
        trans: false,
        dst_regs: vec![
            reg("%r0", PtxType::B32),
            reg("%r1", PtxType::B32),
            reg("%r2", PtxType::B32),
            reg("%r3", PtxType::B32),
        ],
        src_addr: Operand::Register(reg("%rd0", PtxType::U64)),
    }];
    // Only check tensor-core operand validation (no register-lifetime noise).
    let mut has_ldmatrix_err = false;
    for e in &validate_ir_instructions(&instructions).errors {
        if e.message.contains("ldmatrix") {
            has_ldmatrix_err = true;
        }
    }
    assert!(
        !has_ldmatrix_err,
        "valid x4 ldmatrix must not produce an ldmatrix operand error"
    );
}