Skip to content

gen_trace.py

<<<<<<< HEAD

Script to generate human-readable instruction traces for Snitch.

This script takes a trace generated by a Snitch hart (see snitch_cc.sv) and transforms the additional decode stage info into meaningful annotation.

It also counts and computes various performance metrics for every execution region. An execution region is a sequence of instructions. Every mcycle CSR read instruction in your trace implicitly defines two execution regions, comprising respectively:

  • all instructions executed before the read, up to the previous read or the first executed instruction
  • all instructions executed after the read, up to the next read or the last executed instruction

Performance metrics are appended at the end of the generated trace and can optionally be dumped to a separate JSON file.

It also computes various performance metrics for every DMA transfer, provided that the Snitch core is equipped with a tightly-coupled DMA engine, and the DMA trace logged during simulation is fed to the tool. DMA performance metrics are dumped to a separate JSON file.

disasm_inst(hex_inst, mc_exec='llvm-mc', mc_flags='-disassemble -mcpu=snitch') cached

Disassemble a single RISC-V instruction using llvm-mc.

Source code in util/trace/gen_trace.py
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
@lru_cache
def disasm_inst(hex_inst, mc_exec='llvm-mc', mc_flags='-disassemble -mcpu=snitch'):
    """Disassemble a single RISC-V instruction using llvm-mc."""
    # Reverse the endianness of the hex instruction
    inst_fmt = ' '.join(f'0x{byte:02x}' for byte in bytes.fromhex(hex_inst)[::-1])

    # Use llvm-mc to disassemble the binary instruction
    result = subprocess.run(
        [mc_exec] + mc_flags.split(),
        input=inst_fmt,
        capture_output=True,
        text=True,
        check=True,
    )

    # Extract disassembled instruction from llvm-mc output
    return result.stdout.splitlines()[-1].strip().replace('\t', ' ')

flt_decode(val, fmt)

Interprets the binary encoding of an integer as a FP value.

Parameters:

Name Type Description Default
val int

The integer encoding of the FP variable to decode.

required
fmt int

The floating point number format, as an index into the FLOAT_FMTS array.

required

Returns: The floating point value represented by the input integer.

Source code in util/trace/gen_trace.py
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
def flt_decode(val: int, fmt: int) -> float:
    """Interprets the binary encoding of an integer as a FP value.

    Args:
        val: The integer encoding of the FP variable to decode.
        fmt: The floating point number format, as an index into the
            `FLOAT_FMTS` array.
    Returns:
        The floating point value represented by the input integer.
    """
    # get format and bit vector
    w_exp, w_mnt = FLOAT_FMTS[fmt]
    width = 1 + w_exp + w_mnt
    bitstr = '{:064b}'.format(val)[-width:]
    # print(bitstr)
    # Read bit vector slices
    sgn = -1.0 if bitstr[0] == '1' else 1.0
    mnt = int(bitstr[w_exp + 1:], 2)
    exp_unb = int(bitstr[1:w_exp + 1], 2)
    # derive base and exponent
    bse = int('1' + bitstr[w_exp + 1:], 2) / (2**w_mnt)
    exp_bias = -(2**(w_exp - 1) - 1)
    exp = exp_unb + exp_bias
    # case analysis
    if exp_unb == 2**w_exp - 1:
        return sgn * float('inf' if mnt == 0 else 'nan')
    elif exp_unb == 0 and mnt == 0:
        return sgn * 0.0
    elif exp_unb == 0:
        return float(sgn * mnt / (2**w_mnt) * (2**(exp_bias + 1)))
    else:
        return float(sgn * bse * (2**exp))

flt_fmt(flt, width=6)

Formats a floating-point number rounding to a certain decimal precision.

Parameters:

Name Type Description Default
flt float

The floating-point number to format.

required
width int

The number of significant decimal digits to round to.

6

Returns: The formatted floating-point number as a string.

Source code in util/trace/gen_trace.py
494
495
496
497
498
499
500
501
502
503
504
def flt_fmt(flt: float, width: int = 6) -> str:
    """Formats a floating-point number rounding to a certain decimal precision.

    Args:
        flt: The floating-point number to format.
        width: The number of significant decimal digits to round to.
    Returns:
        The formatted floating-point number as a string.
    """
    fmt = '{:.' + str(width) + '}'
    return fmt.format(flt)

flt_lit(num, fmt, width=6, vlen=1)

Formats an integer encoding into a floating-point literal.

Parameters:

Name Type Description Default
num int

The integer encoding of the floating-point number(s).

required
fmt int

The floating point number format, as an index into the FLOAT_FMTS array.

required
width int

The bitwidth of the floating-point type.

6
vlen int

The number of floating-point numbers packed in the encoding,

1 for SIMD vectors.

1
Source code in util/trace/gen_trace.py
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
def flt_lit(num: int, fmt: int, width: int = 6, vlen: int = 1) -> str:
    """Formats an integer encoding into a floating-point literal.

    Args:
        num: The integer encoding of the floating-point number(s).
        fmt: The floating point number format, as an index into the
            `FLOAT_FMTS` array.
        width: The bitwidth of the floating-point type.
        vlen: The number of floating-point numbers packed in the encoding,
            >1 for SIMD vectors.
    """
    # Divide the binary encoding into individual encodings for each number in the SIMD vector.
    bitwidth = 1 + FLOAT_FMTS[fmt][0] + FLOAT_FMTS[fmt][1]
    vec = [num >> (bitwidth * i) & (2**bitwidth - 1) for i in reversed(range(vlen))]
    # Format each individual float encoding to a string.
    floats = [flt_fmt(flt_decode(val, fmt), width) for val in vec]
    # Represent the encodings as a vector if SIMD.
    if len(floats) > 1:
        return '[{}]'.format(', '.join(floats))
    else:
        return floats[0]

flt_op_fmt(extras, port)

Extracts the floating-point format of an instruction operand.

Parameters:

Name Type Description Default
extras dict

The dictionary containing the instruction's extra information.

required
port int

The index of the floating-point operand.

required
Source code in util/trace/gen_trace.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def flt_op_fmt(extras: dict, port: int) -> int:
    """Extracts the floating-point format of an instruction operand.

    Args:
        extras: The dictionary containing the instruction's extra
            information.
        port: The index of the floating-point operand.
    """
    op_sel = extras['op_sel_{}'.format(port)]
    oper_type = FPU_OPER_TYPES[op_sel]
    if extras['is_store']:
        return LS_TO_FLOAT[extras['ls_size']]
    else:
        if oper_type == 'rd':
            return extras['dst_fmt']
        else:
            return extras['src_fmt']

flt_op_vlen(insn, op_type)

Get the vector length of a floating-point instruction operand.

Parameters:

Name Type Description Default
insn str

Instruction as extracted from the trace line.

required
op_type str

One of the operand types defined in FPU_OPER_TYPES.

required

Returns: The vector length of the operand, greater than one if SIMD.

Source code in util/trace/gen_trace.py
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
def flt_op_vlen(insn: str, op_type: str) -> int:
    """Get the vector length of a floating-point instruction operand.

    Args:
        insn: Instruction as extracted from the trace line.
        op_type: One of the operand types defined in `FPU_OPER_TYPES`.
    Returns:
        The vector length of the operand, greater than one if SIMD.
    """
    global _cached_opcodes
    if _cached_opcodes is None:
        load_opcodes()

    # Cut the instruction after the first space to get the instruction name
    insn = insn.split(' ')[0]

    # Check if operand is a source or a destination
    is_rd = (op_type == 'rd')

    # Get operand parameters from the instruction
    op_params = _cached_opcodes.get(insn, None)

    # Get vector length of operand
    if op_params is not None and op_params != ([''] * 4):
        vlen = int(op_params[3]) if is_rd else int(op_params[1])
    else:
        vlen = 1
    return vlen

flt_oper(insn, extras, port)

Extracts details on the floating-point operand of an instruction.

Parameters:

Name Type Description Default
insn str

The current instruction mnemonic.

required
extras dict

The dictionary containing the instruction's extra information.

required
port int

The index of the floating-point operand.

required

Returns: A tuple containing the operand's register name and a string literal representing the floating-point value.

Source code in util/trace/gen_trace.py
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
def flt_oper(insn: str, extras: dict, port: int) -> (str, str):
    """Extracts details on the floating-point operand of an instruction.

    Args:
        insn: The current instruction mnemonic.
        extras: The dictionary containing the instruction's extra
            information.
        port: The index of the floating-point operand.
    Returns:
        A tuple containing the operand's register name and a string
        literal representing the floating-point value.
    """
    op_sel = extras['op_sel_{}'.format(port)]
    oper_type = FPU_OPER_TYPES[op_sel]

    # Assign default return values
    reg = oper_type
    lit = None

    # If operand comes from accelerator interface, format as integer.
    if oper_type == 'acc':
        reg = 'ac{}'.format(port + 1)
        lit = int_lit(extras['acc_qdata_{}'.format(port)], extras['int_fmt'])
    # If operand is not unspecified, format as floating-point.
    elif oper_type != 'NONE':
        # Get operand vector length, FP format and integer encoding
        vlen = flt_op_vlen(insn, oper_type)
        fmt = flt_op_fmt(extras, port)
        enc = extras['op_{}'.format(port)]
        # Return register name and floating-point literal
        return REG_ABI_NAMES_F[extras[oper_type]], flt_lit(enc, fmt, vlen=vlen)
    return reg, lit

=======

Script to generate human-readable instruction traces for Snitch.

This script takes a trace generated by a Snitch hart (see snitch_cc.sv) and transforms the additional decode stage info into meaningful annotation.

It also counts and computes various performance metrics for every execution region. An execution region is a sequence of instructions. Every mcycle CSR read instruction in your trace implicitly defines two execution regions, comprising respectively:

  • all instructions executed before the read, up to the previous read or the first executed instruction
  • all instructions executed after the read, up to the next read or the last executed instruction

Performance metrics are appended at the end of the generated trace and can optionally be dumped to a separate JSON file.

It also computes various performance metrics for every DMA transfer, provided that the Snitch core is equipped with a tightly-coupled DMA engine, and the DMA trace logged during simulation is fed to the tool. DMA performance metrics are dumped to a separate JSON file.

disasm_inst(hex_inst, mc_exec='llvm-mc', mc_flags='-disassemble -mcpu=snitch') cached

Disassemble a single RISC-V instruction using llvm-mc.

Source code in util/trace/gen_trace.py
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
@lru_cache
def disasm_inst(hex_inst, mc_exec='llvm-mc', mc_flags='-disassemble -mcpu=snitch'):
    """Disassemble a single RISC-V instruction using llvm-mc."""
    # Reverse the endianness of the hex instruction
    inst_fmt = ' '.join(f'0x{byte:02x}' for byte in bytes.fromhex(hex_inst)[::-1])

    # Use llvm-mc to disassemble the binary instruction
    result = subprocess.run(
        [mc_exec] + mc_flags.split(),
        input=inst_fmt,
        capture_output=True,
        text=True,
        check=True,
    )

    # Extract disassembled instruction from llvm-mc output
    return result.stdout.splitlines()[-1].strip().replace('\t', ' ')

flt_decode(val, fmt)

Interprets the binary encoding of an integer as a FP value.

Parameters:

Name Type Description Default
val int

The integer encoding of the FP variable to decode.

required
fmt int

The floating point number format, as an index into the FLOAT_FMTS array.

required

Returns: The floating point value represented by the input integer.

Source code in util/trace/gen_trace.py
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
def flt_decode(val: int, fmt: int) -> float:
    """Interprets the binary encoding of an integer as a FP value.

    Args:
        val: The integer encoding of the FP variable to decode.
        fmt: The floating point number format, as an index into the
            `FLOAT_FMTS` array.
    Returns:
        The floating point value represented by the input integer.
    """
    # get format and bit vector
    w_exp, w_mnt = FLOAT_FMTS[fmt]
    width = 1 + w_exp + w_mnt
    bitstr = '{:064b}'.format(val)[-width:]
    # print(bitstr)
    # Read bit vector slices
    sgn = -1.0 if bitstr[0] == '1' else 1.0
    mnt = int(bitstr[w_exp + 1:], 2)
    exp_unb = int(bitstr[1:w_exp + 1], 2)
    # derive base and exponent
    bse = int('1' + bitstr[w_exp + 1:], 2) / (2**w_mnt)
    exp_bias = -(2**(w_exp - 1) - 1)
    exp = exp_unb + exp_bias
    # case analysis
    if exp_unb == 2**w_exp - 1:
        return sgn * float('inf' if mnt == 0 else 'nan')
    elif exp_unb == 0 and mnt == 0:
        return sgn * 0.0
    elif exp_unb == 0:
        return float(sgn * mnt / (2**w_mnt) * (2**(exp_bias + 1)))
    else:
        return float(sgn * bse * (2**exp))

flt_fmt(flt, width=6)

Formats a floating-point number rounding to a certain decimal precision.

Parameters:

Name Type Description Default
flt float

The floating-point number to format.

required
width int

The number of significant decimal digits to round to.

6

Returns: The formatted floating-point number as a string.

Source code in util/trace/gen_trace.py
494
495
496
497
498
499
500
501
502
503
504
def flt_fmt(flt: float, width: int = 6) -> str:
    """Formats a floating-point number rounding to a certain decimal precision.

    Args:
        flt: The floating-point number to format.
        width: The number of significant decimal digits to round to.
    Returns:
        The formatted floating-point number as a string.
    """
    fmt = '{:.' + str(width) + '}'
    return fmt.format(flt)

flt_lit(num, fmt, width=6, vlen=1)

Formats an integer encoding into a floating-point literal.

Parameters:

Name Type Description Default
num int

The integer encoding of the floating-point number(s).

required
fmt int

The floating point number format, as an index into the FLOAT_FMTS array.

required
width int

The bitwidth of the floating-point type.

6
vlen int

The number of floating-point numbers packed in the encoding,

1 for SIMD vectors.

1
Source code in util/trace/gen_trace.py
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
def flt_lit(num: int, fmt: int, width: int = 6, vlen: int = 1) -> str:
    """Formats an integer encoding into a floating-point literal.

    Args:
        num: The integer encoding of the floating-point number(s).
        fmt: The floating point number format, as an index into the
            `FLOAT_FMTS` array.
        width: The bitwidth of the floating-point type.
        vlen: The number of floating-point numbers packed in the encoding,
            >1 for SIMD vectors.
    """
    # Divide the binary encoding into individual encodings for each number in the SIMD vector.
    bitwidth = 1 + FLOAT_FMTS[fmt][0] + FLOAT_FMTS[fmt][1]
    vec = [num >> (bitwidth * i) & (2**bitwidth - 1) for i in reversed(range(vlen))]
    # Format each individual float encoding to a string.
    floats = [flt_fmt(flt_decode(val, fmt), width) for val in vec]
    # Represent the encodings as a vector if SIMD.
    if len(floats) > 1:
        return '[{}]'.format(', '.join(floats))
    else:
        return floats[0]

flt_op_fmt(extras, port)

Extracts the floating-point format of an instruction operand.

Parameters:

Name Type Description Default
extras dict

The dictionary containing the instruction's extra information.

required
port int

The index of the floating-point operand.

required
Source code in util/trace/gen_trace.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def flt_op_fmt(extras: dict, port: int) -> int:
    """Extracts the floating-point format of an instruction operand.

    Args:
        extras: The dictionary containing the instruction's extra
            information.
        port: The index of the floating-point operand.
    """
    op_sel = extras['op_sel_{}'.format(port)]
    oper_type = FPU_OPER_TYPES[op_sel]
    if extras['is_store']:
        return LS_TO_FLOAT[extras['ls_size']]
    else:
        if oper_type == 'rd':
            return extras['dst_fmt']
        else:
            return extras['src_fmt']

flt_op_vlen(insn, op_type)

Get the vector length of a floating-point instruction operand.

Parameters:

Name Type Description Default
insn str

Instruction as extracted from the trace line.

required
op_type str

One of the operand types defined in FPU_OPER_TYPES.

required

Returns: The vector length of the operand, greater than one if SIMD.

Source code in util/trace/gen_trace.py
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
def flt_op_vlen(insn: str, op_type: str) -> int:
    """Get the vector length of a floating-point instruction operand.

    Args:
        insn: Instruction as extracted from the trace line.
        op_type: One of the operand types defined in `FPU_OPER_TYPES`.
    Returns:
        The vector length of the operand, greater than one if SIMD.
    """
    global _cached_opcodes
    if _cached_opcodes is None:
        load_opcodes()

    # Cut the instruction after the first space to get the instruction name
    insn = insn.split(' ')[0]

    # Check if operand is a source or a destination
    is_rd = (op_type == 'rd')

    # Get operand parameters from the instruction
    op_params = _cached_opcodes.get(insn, None)

    # Get vector length of operand
    if op_params is not None and op_params != ([''] * 4):
        vlen = int(op_params[3]) if is_rd else int(op_params[1])
    else:
        vlen = 1
    return vlen

flt_oper(insn, extras, port)

Extracts details on the floating-point operand of an instruction.

Parameters:

Name Type Description Default
insn str

The current instruction mnemonic.

required
extras dict

The dictionary containing the instruction's extra information.

required
port int

The index of the floating-point operand.

required

Returns: A tuple containing the operand's register name and a string literal representing the floating-point value.

Source code in util/trace/gen_trace.py
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
def flt_oper(insn: str, extras: dict, port: int) -> (str, str):
    """Extracts details on the floating-point operand of an instruction.

    Args:
        insn: The current instruction mnemonic.
        extras: The dictionary containing the instruction's extra
            information.
        port: The index of the floating-point operand.
    Returns:
        A tuple containing the operand's register name and a string
        literal representing the floating-point value.
    """
    op_sel = extras['op_sel_{}'.format(port)]
    oper_type = FPU_OPER_TYPES[op_sel]

    # Assign default return values
    reg = oper_type
    lit = None

    # If operand comes from accelerator interface, format as integer.
    if oper_type == 'acc':
        reg = 'ac{}'.format(port + 1)
        lit = int_lit(extras['acc_qdata_{}'.format(port)], extras['int_fmt'])
    # If operand is not unspecified, format as floating-point.
    elif oper_type != 'NONE':
        # Get operand vector length, FP format and integer encoding
        vlen = flt_op_vlen(insn, oper_type)
        fmt = flt_op_fmt(extras, port)
        enc = extras['op_{}'.format(port)]
        # Return register name and floating-point literal
        return REG_ABI_NAMES_F[extras[oper_type]], flt_lit(enc, fmt, vlen=vlen)
    return reg, lit

c744477... trace: Refactor and prepare for DMA trace generation