VivadoComposer.scala 10.2 KB
Newer Older
1
2
3
//
// Copyright (C) 2016 Jens Korinth, TU Darmstadt
//
4
// This file is part of Tapasco (TPC).
5
//
6
// Tapasco is free software: you can redistribute it and/or modify
7
8
9
10
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
11
// Tapasco is distributed in the hope that it will be useful,
12
13
14
15
16
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
17
// along with Tapasco.  If not, see <http://www.gnu.org/licenses/>.
18
19
20
21
22
23
//
/**
 * @file     VivadoComposer.scala
 * @brief    Composer implementation for Vivado Design Suite.
 * @authors  J. Korinth, TU Darmstadt (jk@esa.cs.tu-darmstadt.de)
 **/
24
25
26
27
28
29
30
31
package de.tu_darmstadt.cs.esa.tapasco.activity.composers
import  de.tu_darmstadt.cs.esa.tapasco.Common
import  de.tu_darmstadt.cs.esa.tapasco.base._
import  de.tu_darmstadt.cs.esa.tapasco.base.tcl._
import  de.tu_darmstadt.cs.esa.tapasco.util._
import  de.tu_darmstadt.cs.esa.tapasco.reports._
import  de.tu_darmstadt.cs.esa.tapasco.dse.Heuristics
import  de.tu_darmstadt.cs.esa.tapasco.filemgmt.FileAssetManager
32
33
34
35
36
37
38
import  java.nio.file._
import  scala.sys.process.{Process, ProcessIO}
import  scala.util.Properties.{lineSeparator => NL}
import  ComposeResult._
import  LogFormatter._

/** Implementation of [[Composer]] for Vivado Design Suite. */
39
class VivadoComposer()(implicit cfg: Configuration, maxThreads: Option[Int]) extends Composer {
40
  import VivadoComposer._
41
  private[this] val logger = de.tu_darmstadt.cs.esa.tapasco.Logging.logger(this.getClass)
42
43
44
45
46
47

  /** @inheritdoc */
  def maxMemoryUsagePerProcess: Int = VIVADO_PROCESS_PEAK_MEM

  /** @inheritdoc */
  def compose(bd: Composition, target: Target, f: Heuristics.Frequency = 0, archFeatures: Seq[Feature] = Seq(),
48
49
      platformFeatures: Seq[Feature] = Seq()) (implicit cfg: Configuration, maxThreads: Option[Int]): Composer.Result = {
    logger.debug("VivadoComposer uses at most {} threads", maxThreads getOrElse "unlimited")
50
51
52
53
54
55
56
    // create output struct
    val files = VivadoComposer.Files(bd, target, f)
    // create output directory
    java.nio.file.Files.createDirectories(files.outdir)
    // create Tcl script
    mkTclScript(fromTemplate = Common.commonDir.resolve("design.master.tcl.template"),
                to           = files.tclFile,
Jens Korinth's avatar
Jens Korinth committed
57
                projectName  = Composer.mkProjectName(bd, target, f),
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
                header       = makeHeader(bd, target, f, archFeatures, platformFeatures),
                target       = target,
                composition  = composition(bd, target))

    logger.info("Vivado starting run {}: output in {}", files.runName: Any, files.logFile)


    // Vivado shell command
    val vivadoCmd = Seq("vivado", "-mode", "batch", "-source", files.tclFile.toString,
        "-notrace", "-nojournal", "-log", files.logFile.toString)
    logger.debug("Vivado shell command: {}", vivadoCmd mkString " ")

    // execute Vivado (max runtime: 1 day)
    val r = InterruptibleProcess(Process(vivadoCmd, files.outdir.toFile),
        waitMillis = Some(24 * 60 * 60 * 1000)).!(io)

    // check retcode
    if (r == InterruptibleProcess.TIMEOUT_RETCODE) {
      logger.error("Vivado timeout for %s in '%s'".format(files.runName, files.outdir))
77
      Composer.Result(Timeout, log = files.log, util = None, timing = None, power = None)
78
79
80
81
    } else if (r != 0) {
      logger.error("Vivado finished with non-zero exit code: %d for %s in '%s'"
        .format(r, files.runName, files.outdir))
      Composer.Result(files.log map (_.result) getOrElse OtherError, log = files.log,
82
          util = None, timing = None, power = None)
83
84
85
86
87
    } else {
      // check for timing failure
      if (files.tim.isEmpty) {
        throw new Exception("could not parse timing report: '%s'".format(files.timFile.toString))
      } else {
88
        Composer.Result(checkTimingFailure(files), Some(files.bitFile.toString),
89
          files.log, files.util, files.tim, files.pwr)
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
      }
    }
  }

  /** @inheritdoc */
  def clean(bd: Composition, target: Target, f: Double = 0)(implicit cfg: Configuration): Unit = {
    Common.getFiles(cfg.outputDir(bd, target, f).resolve("bit").toFile).filter(_.isFile).map(_.delete)
    Common.getFiles(cfg.outputDir(bd, target, f).resolve("bit").toFile).filter(_.isDirectory).map(_.deleteOnExit)
    Common.getFiles(cfg.outputDir(bd, target, f).resolve("user_ip").toFile).filter(_.isFile).map(_.delete)
    Common.getFiles(cfg.outputDir(bd, target, f).resolve("user_ip").toFile).filter(_.isDirectory).map(_.deleteOnExit)
  }

  /** @inheritdoc */
  def cleanAll(bd: Composition, target: Target, f: Double = 0)(implicit cfg: Configuration): Unit = {
    Common.getFiles(cfg.outputDir(bd, target, f).toFile).filter(_.isFile).map(_.delete)
    Common.getFiles(cfg.outputDir(bd, target, f).toFile).filter(_.isDirectory).map(_.deleteOnExit)
  }

  /** Check for timing failure in report. */
  private def checkTimingFailure(files: Files): ComposeResult = {
    val wns = files.tim map (_.worstNegativeSlack) getOrElse Double.NegativeInfinity
    if (wns < SLACK_THRESHOLD) {
      logger.error("Vivado finished, but did not achieve timing closure for %s, WNS: %1.3f, max delay path: %s in '%s'"
        .format(files.runName, wns, files.tim.map(_.maxDelayPath), files.outdir))
      TimingFailure
    } else {
      logger.info("Vivado finished successfully for %s, WNS: %1.3f, bitstream file is here: '%s'"
        .format(files.runName, wns, files.bitFile))
      Success
    }
  }

  /** Writes the .tcl script for Vivado. */
  private def mkTclScript(fromTemplate: Path, to: Path, projectName: String, header: String, target: Target,
      composition: String): Unit = {
    // needles for template
    val needles: scala.collection.mutable.Map[String, String] = scala.collection.mutable.Map(
127
      "PROJECT_NAME"     -> "[tapasco::get_generate_mode]",
128
129
130
131
      "BITSTREAM_NAME"   -> projectName,
      "HEADER"           -> header,
      "PRELOAD_FILES"    -> "",
      "PART"             -> target.pd.part,
132
133
      "BOARD_PART"       -> (target.pd.boardPart getOrElse ""),
      "BOARD_PRESET"     -> (target.pd.boardPreset getOrElse ""),
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
      "PLATFORM_TCL"     -> target.pd.tclLibrary.toString,
      "ARCHITECTURE_TCL" -> target.ad.tclLibrary.toString,
      "COMPOSITION"      -> composition
    )

    // write Tcl script
    Template.interpolateFile(
      Template.DEFAULT_NEEDLE,
      fromTemplate.toString,
      to.toString,
      needles)
  }

  /** Produces the Tcl dictionary for the given composition and the IP catalog setup code for Vivado. **/
  private def composition(bd: Composition, target: Target): String = {
    // find all cores
    val cores = (for {
      ce <- bd.composition
    } yield ce.kernel -> FileAssetManager.entities.core(ce.kernel, target)).toMap
    // check that all cores are found, else abort
    if (cores.values map (_.isEmpty) reduce (_ || _)) {
      throw new Exception("could not find all required cores for target %s, missing: %s"
          .format(target, cores filter (_._2.isEmpty) map (_._1) mkString ", "))
    }

    val elems = for {
      ce <- bd.composition
      cd <- cores(ce.kernel)
      vl = VLNV.fromZip(cd.zipPath)
    } yield (ce.kernel, ce.count, cd, cd.zipPath, vl)

    val repoPaths =
      "set_property IP_REPO_PATHS \"[pwd]/user_ip " + Common.commonDir + "\" [current_project]" + NL +
      "file delete -force [pwd]/user_ip" + NL +
      "file mkdir [pwd]/user_ip" + NL +
      "update_ip_catalog" + NL +
      elems.map(_._4.toString).map(zp => "update_ip_catalog -add_ip " + zp + " -repo_path ./user_ip").mkString(NL) + NL +
      "update_ip_catalog" + NL

    repoPaths + (for (i <- 0 until elems.length) yield
      List(
175
        //"update_ip_catalog -add_ip {" + elems(i)._4.toString + "} -repo_path [pwd]/[tapasco::get_generate_mode]",
176
177
178
179
180
181
182
183
184
185
        "dict set kernels {" + i + "} vlnv {" + elems(i)._5 + "}",
        "dict set kernels {" + i + "} count {" + elems(i)._2 + "}",
        "dict set kernels {" + i + "} id {" + elems(i)._3.id + "}",
        ""
     ).mkString(NL)).mkString(NL)
  }

  /** Produces the header section of the main Tcl file, containing several global vars. **/
  private def makeHeader(bd: Composition, target: Target, f: Heuristics.Frequency, archFeatures: Seq[Feature],
      platformFeatures: Seq[Feature]): String =
Jens Korinth's avatar
Jens Korinth committed
186
    "set tapasco_freq %3.0f%s".format(f, NL) +
187
    (target.pd.boardPreset map (bp => "set tapasco_board_preset %s%s".format(bp, NL)) getOrElse "") +
188
    (maxThreads map (mt => "set tapasco_jobs %d%s".format(mt, NL)) getOrElse "") +
Jens Korinth's avatar
Jens Korinth committed
189
    (maxThreads map (mt => "set_param general.maxThreads %d%s".format(mt, NL)) getOrElse "") +
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
    (platformFeatures.map { f => new FeatureTclPrinter("platform").toTcl(f) } mkString NL) +
    (archFeatures.map { f => new FeatureTclPrinter("architecture").toTcl(f) } mkString NL) + NL
}

/** Companion object of [[VivadoComposer]]. */
object VivadoComposer {
  /** peak memory requirements **/
  final val VIVADO_PROCESS_PEAK_MEM: Int = 15
  /** Slack threshold for WNS relaxation. */
  final val SLACK_THRESHOLD: Double = -0.3

  /** Output files and directories for a run. */
  private final case class Files(c: Composition, t: Target, f: Heuristics.Frequency)
                                (implicit cfg: Configuration) {
    lazy val outdir: Path    = cfg.outputDir(c, t, f)
    lazy val logFile: Path   = outdir.resolve("%s.log".format(c.id))
    lazy val tclFile: Path   = outdir.resolve("%s.tcl".format(t.pd.name))
    lazy val bitFile: Path   = outdir.resolve("%s.bit".format(c.id))
    lazy val runName: String = "%s with %s[F=%1.3f]".format(logformat(c), t, f)
    lazy val pwrFile: Path   = logFile.resolveSibling("power.txt")
    lazy val timFile: Path   = logFile.resolveSibling("timing.txt")
211
    lazy val utilFile: Path  = logFile.resolveSibling("utilization.txt")
212
213
214
    lazy val log             = ComposerLog(logFile)
    lazy val pwr             = PowerReport(pwrFile)
    lazy val tim             = TimingReport(timFile)
215
    lazy val util            = UtilizationReport(utilFile)
216
217
218
219
220
221
222
223
224
  }

  /** custom ProcessIO: ignore everything. */
  private final val io = new ProcessIO(
    stdin => {stdin.close()},
    stdout => {stdout.close()},
    stderr => {stderr.close()}
  )
}