Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions linker/slashkit/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from slashkit.emit.emu.project_gen import build_emu_project, package_emu_artifacts

from slashkit.emit.metadata.prog_image import build_vbin
from slashkit.emit.metadata.timing_freq import apply_timing_frequency_cap
from slashkit.core.command_config import LinkerConfiguration, Platform, InstallerConfiguration, CommandConfiguration


Expand Down Expand Up @@ -143,6 +144,18 @@ def link(config: LinkerConfiguration) -> None:
elif config.platform == Platform.EMULATION:
package_emu_artifacts(config)
else:
# Inspect the achieved user-clock frequency from the RM timing report and
# cap the value recorded in system_map.xml at the requested target, before
# it is packed into the vbin.
timing_report = (
config.build_dir / "slash_rm" /
f"report_timing_{config.project_name}.txt"
)
apply_timing_frequency_cap(
project_name=config.project_name,
system_map_path=config.build_dir / "system_map.xml",
timing_report=timing_report,
)
generate_util_report(config)
build_vbin(config)

Expand Down
2 changes: 1 addition & 1 deletion linker/slashkit/core/command_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def populate_argument_parser(cls, ap: argparse.ArgumentParser):
ap.add_argument("--pre-synth-tcls", type=Path, nargs="*", default=[],
help="Paths to TCL scripts to run before synthesis (applies to hardware builds only).")
ap.add_argument("--clock-hz", required=False,
type=int, default=None, help="Target clock frequency in MHz.")
type=int, default=None, help="Target clock frequency in Hz.")

def __init__(self, args: argparse.Namespace):
super().__init__(args)
Expand Down
33 changes: 32 additions & 1 deletion linker/slashkit/emit/hw/project_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from slashkit.emit.metadata.report_util import convert_report_utilization_to_xml
from slashkit.emit.render import export_package
from slashkit.core.command_config import LinkerConfiguration, InstallerConfiguration, CommandConfiguration
from slashkit.emit.metadata.timing_freq import require_static_shell_timing_or_confirm
from slashkit.emit.metadata.timing_freq import require_static_shell_timing_or_confirm, read_system_map_clock_hz

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -313,6 +313,33 @@ class RM_KIND(Enum):
SERVICE_LAYER = "service_layer"


def _generate_user_clock_xdc(config: LinkerConfiguration) -> Optional[Path]:
# Turn the resolved target user-clock frequency into an actual Vivado timing
# constraint for the reconfigurable-module build. The resolved target has
# already been written to system_map.xml by generate_tcl(), so we read it
# back here to keep a single source of truth (see resolve_system_map_clock).
system_map_path = config.build_dir / "system_map.xml"
target_hz = read_system_map_clock_hz(system_map_path)
if target_hz is None or target_hz <= 0:
logger.warning(
"No valid target ClockFrequency in %s; skipping user-clock constraint",
system_map_path,
)
return None

period_ns = 1e9 / float(target_hz)
xdc_path = config.build_dir / "user_clock.xdc"
# NOTE: the clock object below (the user_clk port, scoped to top_i/slash in
# the RM build) must be validated against Vivado.
xdc_path.write_text(
f"create_clock -name user_clk -period {period_ns:.6f} [get_ports user_clk]\n",
encoding="utf-8",
)
logger.info("Wrote user-clock constraint (%.6f ns / %d Hz) to %s",
period_ns, target_hz, xdc_path)
return xdc_path


def _run_rm_build(config: LinkerConfiguration, rm_kind: RM_KIND) -> None:
if rm_kind == RM_KIND.SLASH_PROJECT:
# Copy all base IP cores into the ip repository
Expand Down Expand Up @@ -397,6 +424,10 @@ def _run_rm_build(config: LinkerConfiguration, rm_kind: RM_KIND) -> None:
for path in config.pre_synth_tcls:
cmd.extend(["--pre-synth-tcl", str(path)])

user_clock_xdc = _generate_user_clock_xdc(config)
if user_clock_xdc is not None:
cmd.extend(["--user-clock-xdc", str(user_clock_xdc)])

if rm_kind == RM_KIND.SERVICE_LAYER:
opt_post_tcl = stack.enter_context(
resources.path(
Expand Down
35 changes: 26 additions & 9 deletions linker/slashkit/emit/metadata/timing_freq.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ def apply_timing_frequency_cap(
*,
project_name: str,
system_map_path: Path,
base_freq_hz: int = 400_000_000,
base_freq_hz: Optional[int] = None,
timing_report: Optional[Path] = None,
hw_build_dir: Optional[Path] = None,
) -> Optional[int]:
user_clock_hz = read_system_map_clock_hz(system_map_path)
Expand All @@ -242,17 +243,33 @@ def apply_timing_frequency_cap(
"ClockFrequency missing or invalid in system_map.xml: %s", system_map_path)
return None

resolved_hw_build_dir = _resolve_hw_build_dir(hw_build_dir)
if resolved_hw_build_dir is None:
logger.warning(
"HW build directory env var is unset; keeping user clock_hz=%d", user_clock_hz)
return user_clock_hz
# The user region is implemented against the target period, so the WNS in
# the timing report is measured relative to the target frequency. Default the
# analysis base to the target so the computed achievable frequency is correct.
if base_freq_hz is None:
base_freq_hz = user_clock_hz

timing_report = _find_timing_report(project_name, resolved_hw_build_dir)
if timing_report is None:
resolved_hw_build_dir = _resolve_hw_build_dir(hw_build_dir)
if resolved_hw_build_dir is None:
logger.warning(
"HW build directory env var is unset; keeping user clock_hz=%d", user_clock_hz)
return user_clock_hz

timing_report = _find_timing_report(
project_name, resolved_hw_build_dir)
if timing_report is None:
logger.warning(
"Timing report not found under %s for project %s; keeping user clock_hz=%d",
resolved_hw_build_dir,
project_name,
user_clock_hz,
)
return user_clock_hz
elif not timing_report.is_file():
logger.warning(
"Timing report not found under %s for project %s; keeping user clock_hz=%d",
resolved_hw_build_dir,
"Timing report %s not found for project %s; keeping user clock_hz=%d",
timing_report,
project_name,
user_clock_hz,
)
Expand Down
17 changes: 17 additions & 0 deletions linker/slashkit/resources/base/scripts/slash_project_build.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ array set opts {
--rm-work-dir ""
--artifact-out-dir ""
--util-report-file ""
--user-clock-xdc ""
--jobs 8
}

Expand Down Expand Up @@ -89,6 +90,10 @@ set linker_results_dir [file normalize $opts(--linker-results-dir)]
set rm_work_dir $opts(--rm-work-dir)
set artifact_out_dir $opts(--artifact-out-dir)
set util_report_file $opts(--util-report-file)
set user_clock_xdc $opts(--user-clock-xdc)
if {$user_clock_xdc ne ""} {
set user_clock_xdc [file normalize $user_clock_xdc]
}
set jobs $opts(--jobs)

file mkdir $rm_work_dir
Expand Down Expand Up @@ -157,6 +162,18 @@ foreach pre_synth_tcl $pre_synth_tcls {
source $pre_synth_tcl
}

# Apply the user-region clock timing constraint (create_clock on user_clk),
# derived from the requested --clock-hz target. Scoped to the slash cell so it
# constrains the reconfigurable module's user clock. NOTE: the clock object and
# scoping below must be validated against Vivado.
if {$user_clock_xdc ne ""} {
puts "Applying user-clock constraint: $user_clock_xdc"
_require_file $user_clock_xdc "user-clock XDC"
add_files -fileset constrs_1 -norecurse $user_clock_xdc
set_property USED_IN {synthesis implementation} [get_files $user_clock_xdc]
set_property SCOPED_TO_CELLS {top_i/slash} [get_files $user_clock_xdc]
}

launch_runs "${slash_rm_name}_synth_1" -jobs $jobs
wait_on_run "${slash_rm_name}_synth_1"

Expand Down
Empty file.
106 changes: 106 additions & 0 deletions linker/test/emit/metadata/test_timing_freq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ##################################################################################################
# The MIT License (MIT)
# Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# ##################################################################################################

"""Tests for apply_timing_frequency_cap — achieved-frequency capping at target."""

import textwrap
from pathlib import Path

from slashkit.emit.metadata import timing_freq


def _write_system_map(path: Path, clock_hz: int) -> None:
path.write_text(
f'<?xml version="1.0"?>\n<SystemMap>\n'
f' <ClockFrequency>{clock_hz}</ClockFrequency>\n</SystemMap>\n',
encoding="utf-8",
)


def _write_timing_report(path: Path, wns_ns: float) -> None:
# Minimal shape matching what extract_design_wns_ns() scans for: a
# "Design Timing Summary" section, a WNS(ns)/TNS(ns) header, then a data row
# whose first value is WNS and third is WHS.
path.write_text(
textwrap.dedent(
f"""\
Design Timing Summary
---------------------

WNS(ns) TNS(ns) WHS(ns) THS(ns)
------- ------- ------- -------
{wns_ns:.3f} 0.000 0.100 0.000
"""
),
encoding="utf-8",
)


def test_positive_slack_is_capped_at_target(tmp_path):
# Target 250 MHz (4 ns). Positive WNS (headroom) would allow a higher
# frequency, but the result must be capped at the requested target.
target_hz = 250_000_000
system_map = tmp_path / "system_map.xml"
report = tmp_path / "report_timing_proj.txt"
_write_system_map(system_map, target_hz)
_write_timing_report(report, wns_ns=1.0) # achievable ~333 MHz

result = timing_freq.apply_timing_frequency_cap(
project_name="proj",
system_map_path=system_map,
timing_report=report,
)

assert result == target_hz
assert timing_freq.read_system_map_clock_hz(system_map) == target_hz


def test_negative_slack_lowers_below_target(tmp_path):
# Target 250 MHz (4 ns period). Negative WNS means timing failed: the
# achievable frequency is 1e9 / (4 - (-1)) = 200 MHz, below the target.
target_hz = 250_000_000
system_map = tmp_path / "system_map.xml"
report = tmp_path / "report_timing_proj.txt"
_write_system_map(system_map, target_hz)
_write_timing_report(report, wns_ns=-1.0)

result = timing_freq.apply_timing_frequency_cap(
project_name="proj",
system_map_path=system_map,
timing_report=report,
)

assert result == 200_000_000
assert timing_freq.read_system_map_clock_hz(system_map) == 200_000_000


def test_missing_report_keeps_target(tmp_path):
target_hz = 300_000_000
system_map = tmp_path / "system_map.xml"
_write_system_map(system_map, target_hz)

result = timing_freq.apply_timing_frequency_cap(
project_name="proj",
system_map_path=system_map,
timing_report=tmp_path / "does_not_exist.txt",
)

assert result == target_hz
assert timing_freq.read_system_map_clock_hz(system_map) == target_hz
1 change: 0 additions & 1 deletion vrt/include/vrt/device.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ namespace impl {
class Device {
static constexpr uint64_t QDMA_LOGIC_BASE = 0x20100020000; ///< Base address for QDMA logic
static constexpr uint32_t QDMA_LOGIC_OFFSET = 0x1000; /// Offset for QDMA logic
static constexpr uint32_t CLOCK_MAX_FREQ = 333333333;
uint8_t bar = 0; ///< Base Address Register (BAR)
uint64_t offset = 0; ///< Offset for memory operations
uint16_t pci_bdf = 0; ///< PCI Bus:Device.Function identifier
Expand Down
17 changes: 12 additions & 5 deletions vrt/src/device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,19 @@ Device::Device(const std::string& bdf, const std::string& vrtbinPath, bool progr
sleep(1); // wait for device to be ready after programming before accessing BAR

}
if (vrtdDevice.has_value()) {
if (clockFreq > CLOCK_MAX_FREQ) {
utils::Logger::log(utils::LogLevel::WARN, __PRETTY_FUNCTION__,
"Clock frequency {} exceeds maximum frequency {}", clockFreq, CLOCK_MAX_FREQ);
vrtdDevice->setUserClockRate(static_cast<uint32_t>(CLOCK_MAX_FREQ));
if (program && vrtdDevice.has_value() && clockFreq > 0) {
// Program the user clock to the achieved (timed) frequency recorded
// in the vbin, rounding down so the user logic never runs faster
// than it was timed for. The user may still override afterwards via
// setFrequency().
if (clockFreq > std::numeric_limits<uint32_t>::max()) {
throw std::runtime_error(
"Achieved clock frequency exceeds vrtd clock API limits");
}
uint32_t achieved =
vrtdDevice->setUserClockRateRoundDown(static_cast<uint32_t>(clockFreq));
utils::Logger::log(utils::LogLevel::INFO, __PRETTY_FUNCTION__,
"Programmed user clock to {} Hz (target {} Hz)", achieved, clockFreq);
}
} else if (platform == Platform::EMULATION) {
parseSystemMap();
Expand Down
4 changes: 4 additions & 0 deletions vrt/vrtd/include/vrtd/wire.h
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,10 @@ enum vrtd_clock_region {
enum vrtd_clock_op {
VRTD_CLOCK_OP_GET = 0,
VRTD_CLOCK_OP_SET = 1,
/* Like SET, but never selects a realizable rate above the request. Used to
* program the user clock to the achieved (timed) frequency at load without
* ever running the user logic faster than it was timed for. */
VRTD_CLOCK_OP_SET_ROUND_DOWN = 2,
};

/**
Expand Down
24 changes: 24 additions & 0 deletions vrt/vrtd/libvrtd/include/vrtd/vrtd.h
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,30 @@ enum vrtd_ret vrtd_clock_set_rate(
uint32_t *rate_hz_out
);

/**
* @brief Set the clock rate for a device region, never exceeding the request.
*
* Like vrtd_clock_set_rate(), but the daemon only selects a realizable rate
* less than or equal to @p rate_hz_in. Used to program the user clock to the
* achieved (timed) frequency without ever running the logic faster than timed.
*
* @param fd Connected vrtd socket file descriptor.
* @param dev Device index (0‑based).
* @param region One of vrtd_clock_region.
* @param rate_hz_in Requested rate in Hz (upper bound).
* @param rate_hz_out Output pointer for the achieved rate in Hz.
*
* @return #VRTD_RET_OK on success; otherwise a #vrtd_ret error code.
* @pre @p rate_hz_out must not be NULL.
*/
enum vrtd_ret vrtd_clock_set_rate_round_down(
int fd,
uint32_t dev,
uint32_t region,
uint32_t rate_hz_in,
uint32_t *rate_hz_out
);


struct vrtd_buffer {
int sock_fd;
Expand Down
Loading
Loading