diff mbox series

[bug#52283,02/10] transformations: Add '--tune'.

Message ID 20211204204924.15581-2-ludo@gnu.org
State Accepted
Headers show
Series Tuning packages for CPU micro-architectures | expand

Checks

Context Check Description
cbaines/applying patch fail View Laminar job
cbaines/issue success View issue
cbaines/applying patch fail View Laminar job
cbaines/issue success View issue
cbaines/applying patch fail View Laminar job
cbaines/issue success View issue

Commit Message

Ludovic Courtès Dec. 4, 2021, 8:49 p.m. UTC
From: Ludovic Courtès <ludovic.courtes@inria.fr>

* guix/transformations.scm (tuning-compiler)
(tuned-package, tunable-package?, package-tuning)
(transform-package-tuning): New procedures.
(%transformations): Add 'tune'.
(%transformation-options): Add "--tune".
* tests/transformations.scm ("options->transformation, tune"): New
test.
* doc/guix.texi (Package Transformation Options): Document '--tune'.
---
 doc/guix.texi             |  54 +++++++++++++++
 guix/transformations.scm  | 134 ++++++++++++++++++++++++++++++++++++++
 tests/transformations.scm |  20 ++++++
 3 files changed, 208 insertions(+)

Comments

Thiago Jung Bauermann Dec. 6, 2021, 11:18 p.m. UTC | #1
Hello Ludo,

Awesome series! I only have comments about this patch, and then only minor 
ones:

Em sábado, 4 de dezembro de 2021, às 17:49:16 -03, Ludovic Courtès 
escreveu:
> +Tuned packages are @emph{grafted} onto packages that depend on them
> +(@pxref{Security Updates, grafts}).  Thus, using @option{--no-grafts}
> +annihilates the effect of @option{--tune}.

Perhaps this is because English isn’t my first language, but annihilation 
seems like a violent and dramatic effect in a package transformation. :-)

Perhaps reword as “cancels”, “invalidates” or "nullifies"?

> +(define (tuned-package p micro-architecture)
> +  "Return package P tuned for MICRO-ARCHITECTURE."
> +  (define compiler
> +    (tuning-compiler micro-architecture))
> +
> +  (package
> +    (inherit p)
> +    (native-inputs
> +     ;; Arrange so that COMPILER comes first in $PATH.
> +     `(("tuning-compiler" ,compiler)
> +       ,@(package-native-inputs p)))
> +    (arguments
> +     (substitute-keyword-arguments (package-arguments p)
> +       ((#:tests? _ #f) #f)))

Perhaps I’m reading this wrong, but it looks like tuned packages don’t run 
their testsuites? If so, this is a surprising side-effect and thus it would 
be nice to have it mentioned in the manual, possibly also in a comment 
here. It would be nice to also mention the rationale for disabling the 
tests (not sure whether only in a comment here or if in the manual as 
well). I assume it’s for convenience, but I’m not sure.
Ludovic Courtès Dec. 7, 2021, 8:04 a.m. UTC | #2
Hi Thiago,

Thiago Jung Bauermann <bauermann@kolabnow.com> skribis:

> Em sábado, 4 de dezembro de 2021, às 17:49:16 -03, Ludovic Courtès 
> escreveu:
>> +Tuned packages are @emph{grafted} onto packages that depend on them
>> +(@pxref{Security Updates, grafts}).  Thus, using @option{--no-grafts}
>> +annihilates the effect of @option{--tune}.
>
> Perhaps this is because English isn’t my first language, but annihilation 
> seems like a violent and dramatic effect in a package transformation. :-)
>
> Perhaps reword as “cancels”, “invalidates” or "nullifies"?

Not a native speaker either but yes, “cancels” sounds better; I’ll
change that.

>> +(define (tuned-package p micro-architecture)
>> +  "Return package P tuned for MICRO-ARCHITECTURE."
>> +  (define compiler
>> +    (tuning-compiler micro-architecture))
>> +
>> +  (package
>> +    (inherit p)
>> +    (native-inputs
>> +     ;; Arrange so that COMPILER comes first in $PATH.
>> +     `(("tuning-compiler" ,compiler)
>> +       ,@(package-native-inputs p)))
>> +    (arguments
>> +     (substitute-keyword-arguments (package-arguments p)
>> +       ((#:tests? _ #f) #f)))
>
> Perhaps I’m reading this wrong, but it looks like tuned packages don’t run 
> their testsuites? If so, this is a surprising side-effect and thus it would 
> be nice to have it mentioned in the manual, possibly also in a comment 
> here. It would be nice to also mention the rationale for disabling the 
> tests (not sure whether only in a comment here or if in the manual as 
> well). I assume it’s for convenience, but I’m not sure.

I agree, a comment and maybe a sentence in the manual would be welcome.

The reason the test suite is skipped is because we cannot know for sure
whether the machine that hosts the daemon is able to run code for this
specific micro-architecture.

The test suite runs in the “baseline” package build anyway, so assuming
the compiler works fine, skipping the test suite on tuned builds is
okay.

Thanks for your feedback!

Ludo’.
Simon Tournier Dec. 7, 2021, 10:32 a.m. UTC | #3
Hi,

On Tue, 7 Dec 2021 at 09:06, Ludovic Courtès <ludovic.courtes@inria.fr> wrote:

> The reason the test suite is skipped is because we cannot know for sure
> whether the machine that hosts the daemon is able to run code for this
> specific micro-architecture.

Naive question: is it possible to effectively run it via emulation?

> The test suite runs in the “baseline” package build anyway, so assuming
> the compiler works fine, skipping the test suite on tuned builds is
> okay.

I miss if the test suite is effectively run somewhere?  And "baseline"
package build means the package built for generic architecture, right?


Cheers,
simon

PS:
My questions are coming from Julia packages in mind, where the test
suite is the only way to know all is fine.  And many times, add System
Image for Julia had been discussed and basically this System Image is
precompilation (generic one or specialized for micro-architecture).
Therefore, maybe this new 'tune' transformation would fit the bill.
:-)

https://docs.julialang.org/en/v1/devdocs/sysimg/
Ludovic Courtès Dec. 7, 2021, 2:52 p.m. UTC | #4
zimoun <zimon.toutoune@gmail.com> skribis:

> On Tue, 7 Dec 2021 at 09:06, Ludovic Courtès <ludovic.courtes@inria.fr> wrote:
>
>> The reason the test suite is skipped is because we cannot know for sure
>> whether the machine that hosts the daemon is able to run code for this
>> specific micro-architecture.
>
> Naive question: is it possible to effectively run it via emulation?

Not to my knowledge.

>> The test suite runs in the “baseline” package build anyway, so assuming
>> the compiler works fine, skipping the test suite on tuned builds is
>> okay.
>
> I miss if the test suite is effectively run somewhere?

Yes, for the default/generic/baseline package, when not using ‘--tune’.

> And "baseline" package build means the package built for generic
> architecture, right?

Correct.

> My questions are coming from Julia packages in mind, where the test
> suite is the only way to know all is fine.  And many times, add System
> Image for Julia had been discussed and basically this System Image is
> precompilation (generic one or specialized for micro-architecture).
> Therefore, maybe this new 'tune' transformation would fit the bill.
> :-)
>
> https://docs.julialang.org/en/v1/devdocs/sysimg/

According to this page, ‘--tune’ won’t be necessary here because Julia
supports function multi-versioning for its “system image”:

  The system image can be compiled simultaneously for multiple CPU
  microarchitectures under the same instruction set architecture (ISA).
  Multiple versions of the same function may be created with minimum
  dispatch point inserted into shared functions in order to take
  advantage of different ISA extensions or other microarchitecture
  features.  The version that offers the best performance will be
  selected automatically at runtime based on available CPU features.

I guess we should follow the instructions at
<https://docs.julialang.org/en/v1/devdocs/sysimg/#Specifying-multiple-system-image-targets>
to build a system image that contains multiple versions of each
function.

Thanks,
Ludo’.
Simon Tournier Dec. 7, 2021, 3:52 p.m. UTC | #5
Hi,

On Tue, 7 Dec 2021 at 15:52, Ludovic Courtès <ludovic.courtes@inria.fr> wrote:
> zimoun <zimon.toutoune@gmail.com> skribis:

> >> The test suite runs in the “baseline” package build anyway, so assuming
> >> the compiler works fine, skipping the test suite on tuned builds is
> >> okay.
> >
> > I miss if the test suite is effectively run somewhere?
>
> Yes, for the default/generic/baseline package, when not using ‘--tune’.

Assuming, the default/generic/baseline package is effectively built. :-)

I imagine the scenario: I develop a new simulation tool, I package it
for Guix, I share it; usually I run "guix shell -D" and do loop over
"make" and "make check", then deploy using "guix build --tune".  My
colleague fetches it and want to run it on another cluster, i.e., they
run "guix build --tune".  The test suite for the generic/baseline is
never run inside a clean environment.  And as we know, this isolated
part allows to detect many common issues; which are often source of
"it works for me, why does it not work for you?". ;-)


> > My questions are coming from Julia packages in mind, where the test
> > suite is the only way to know all is fine.  And many times, add System
> > Image for Julia had been discussed and basically this System Image is
> > precompilation (generic one or specialized for micro-architecture).
> > Therefore, maybe this new 'tune' transformation would fit the bill.
> > :-)
> >
> > https://docs.julialang.org/en/v1/devdocs/sysimg/
>
> According to this page, ‘--tune’ won’t be necessary here because Julia
> supports function multi-versioning for its “system image”:

Yes, but from my understanding, the "baseline" cannot provide an image
for all the micro-architectures, but only 'generic'.  Moreover, as you
described elsewhere, we cannot know for sure whether the machine that
hosts the daemon is able to run code for this specific
micro-architecture.  Anyway.  That's off topic. ;-)  Thanks for
explaining and let discuss elsewhere this Julia machinery. :-)


Cheers,
simon
Ludovic Courtès Dec. 9, 2021, 9:19 a.m. UTC | #6
zimoun <zimon.toutoune@gmail.com> skribis:

> On Tue, 7 Dec 2021 at 15:52, Ludovic Courtès <ludovic.courtes@inria.fr> wrote:
>> zimoun <zimon.toutoune@gmail.com> skribis:

[...]

>> > I miss if the test suite is effectively run somewhere?
>>
>> Yes, for the default/generic/baseline package, when not using ‘--tune’.
>
> Assuming, the default/generic/baseline package is effectively built. :-)
>
> I imagine the scenario: I develop a new simulation tool, I package it
> for Guix, I share it; usually I run "guix shell -D" and do loop over
> "make" and "make check", then deploy using "guix build --tune".  My
> colleague fetches it and want to run it on another cluster, i.e., they
> run "guix build --tune".  The test suite for the generic/baseline is
> never run inside a clean environment.  And as we know, this isolated
> part allows to detect many common issues; which are often source of
> "it works for me, why does it not work for you?". ;-)

Sure, we can always come up with such scenarios.

>> > My questions are coming from Julia packages in mind, where the test
>> > suite is the only way to know all is fine.  And many times, add System
>> > Image for Julia had been discussed and basically this System Image is
>> > precompilation (generic one or specialized for micro-architecture).
>> > Therefore, maybe this new 'tune' transformation would fit the bill.
>> > :-)
>> >
>> > https://docs.julialang.org/en/v1/devdocs/sysimg/
>>
>> According to this page, ‘--tune’ won’t be necessary here because Julia
>> supports function multi-versioning for its “system image”:
>
> Yes, but from my understanding, the "baseline" cannot provide an image
> for all the micro-architectures, but only 'generic'.  Moreover, as you
> described elsewhere, we cannot know for sure whether the machine that
> hosts the daemon is able to run code for this specific
> micro-architecture.

With multi-versioning, the system image (AIUI) provides several versions
of the relevant code, one for each useful micro-architecture.  Such a
system image can be used anywhere because the right version of the code
will be picked up at run-time depending on the host CPU.

It’s The Right Thing, so no worries here!  We can take advantage of that
feature in our Julia package.

Thanks,
Ludo’.
Simon Tournier Dec. 9, 2021, 10:35 a.m. UTC | #7
Hi,

On Thu, 09 Dec 2021 at 10:19, Ludovic Courtès <ludovic.courtes@inria.fr> wrote:
> zimoun <zimon.toutoune@gmail.com> skribis:

>> I imagine the scenario: I develop a new simulation tool, I package it
>> for Guix, I share it; usually I run "guix shell -D" and do loop over
>> "make" and "make check", then deploy using "guix build --tune".  My
>> colleague fetches it and want to run it on another cluster, i.e., they
>> run "guix build --tune".  The test suite for the generic/baseline is
>> never run inside a clean environment.  And as we know, this isolated
>> part allows to detect many common issues; which are often source of
>> "it works for me, why does it not work for you?". ;-)
>
> Sure, we can always come up with such scenarios.

Turning off the test is the general case to cover various use case.

Does it make sense to conditionally turn off?  Say, the default for
’tune’ is #f, but it is #t when the requested host micro-architecture is
the same than the daemon one.  Well, maybe it is overcomplicated for few
corner cases. :-)


>>> According to this page, ‘--tune’ won’t be necessary here because Julia
>>> supports function multi-versioning for its “system image”:
>>
>> Yes, but from my understanding, the "baseline" cannot provide an image
>> for all the micro-architectures, but only 'generic'.  Moreover, as you
>> described elsewhere, we cannot know for sure whether the machine that
>> hosts the daemon is able to run code for this specific
>> micro-architecture.
>
> With multi-versioning, the system image (AIUI) provides several versions
> of the relevant code, one for each useful micro-architecture.  Such a
> system image can be used anywhere because the right version of the code
> will be picked up at run-time depending on the host CPU.

Thanks for explaining.  Indeed, the “baseline” could provide an image
for all the micro-architectures; if it is not already the case*.  The
blog post [1] refers to LWN article [2]; which underlines the impact on
the resulting image size, it should be minimal.  Benchmark required for
Julia. :-)

1: <https://hpc.guix.info/blog/2018/01/pre-built-binaries-vs-performance/>
2: <https://lwn.net/Articles/691932/>


Cheers,
simon

*not already the case: «As an example, at the time of this writing, the
following string is used in the creation of the official x86_64 Julia
binaries downloadable from julialang.org:»

    generic;sandybridge,-xsaveopt,clone_all;haswell,-rdrnd,base(1)

<https://docs.julialang.org/en/v1/devdocs/sysimg/>

And I do not know exactly if the current situation for the precompiled
.ji is optimal, another story.  Indeed, this tune transformation is not
useful for Julia. :-) Thanks for the patient explanations.
Ludovic Courtès Dec. 10, 2021, 8:49 a.m. UTC | #8
Hello!

zimoun <zimon.toutoune@gmail.com> skribis:

> On Thu, 09 Dec 2021 at 10:19, Ludovic Courtès <ludovic.courtes@inria.fr> wrote:
>> zimoun <zimon.toutoune@gmail.com> skribis:
>
>>> I imagine the scenario: I develop a new simulation tool, I package it
>>> for Guix, I share it; usually I run "guix shell -D" and do loop over
>>> "make" and "make check", then deploy using "guix build --tune".  My
>>> colleague fetches it and want to run it on another cluster, i.e., they
>>> run "guix build --tune".  The test suite for the generic/baseline is
>>> never run inside a clean environment.  And as we know, this isolated
>>> part allows to detect many common issues; which are often source of
>>> "it works for me, why does it not work for you?". ;-)
>>
>> Sure, we can always come up with such scenarios.
>
> Turning off the test is the general case to cover various use case.
>
> Does it make sense to conditionally turn off?  Say, the default for
> ’tune’ is #f, but it is #t when the requested host micro-architecture is
> the same than the daemon one.  Well, maybe it is overcomplicated for few
> corner cases. :-)

Yeah, there’s currently no way to know whether the build machine would
be able to run that code.  Knowing what machine the daemon runs on is
not enough because there could be offloading.

Ludo’.
diff mbox series

Patch

diff --git a/doc/guix.texi b/doc/guix.texi
index a675631b79..e3aca8fd3b 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -10906,6 +10906,60 @@  available options and a synopsis (these options are not shown in the
 
 @table @code
 
+@cindex performance, tuning code
+@cindex optimization, of package code
+@cindex tuning, of package code
+@cindex SIMD support
+@cindex tunable packages
+@cindex package multi-versioning
+@item --tune[=@var{cpu}]
+Use versions of the packages marked as ``tunable'' optimized for
+@var{cpu}.  When @var{cpu} is @code{native}, or when it is omitted, tune
+for the CPU on which the @command{guix} command is running.
+
+Valid @var{cpu} names are those recognized by GCC, the GNU Compiler
+Collection.  On x86_64 processors, this includes CPU names such as
+@code{nehalem}, @code{haswell}, and @code{skylake} (@pxref{x86 Options,
+@code{-march},, gcc, Using the GNU Compiler Collection (GCC)}).
+
+As new generations of CPUs come out, they augment the standard
+instruction set architecture (ISA) with additional instructions, in
+particular instructions for single-instruction/multiple-data (SIMD)
+parallel processing.  For example, while Core2 and Skylake CPUs both
+implement the x86_64 ISA, only the latter supports AVX2 SIMD
+instructions.
+
+The primary gain one can expect from @option{--tune} is for programs
+that can make use of those SIMD capabilities @emph{and} that do not
+already have a mechanism to select the right optimized code at run time.
+Packages that have the @code{tunable?} property set are considered
+@dfn{tunable packages} by the @option{--tune} option; a package
+definition with the property set looks like this:
+
+@lisp
+(package
+  (name "hello-simd")
+  ;; ...
+
+  ;; This package may benefit from SIMD extensions so
+  ;; mark it as "tunable".
+  (properties '((tunable? . #t))))
+@end lisp
+
+Other packages are not considered tunable.  This allows Guix to use
+generic binaries in the cases where tuning for a specific CPU is
+unlikely to provide any gain.
+
+Tuned packages are @emph{grafted} onto packages that depend on them
+(@pxref{Security Updates, grafts}).  Thus, using @option{--no-grafts}
+annihilates the effect of @option{--tune}.
+
+We call this technique @dfn{package multi-versioning}: several variants
+of tunable packages may be built, one for each CPU variant.  It is the
+coarse-grain counterpart of @dfn{function multi-versioning} as
+implemented by the GNU tool chain (@pxref{Function Multiversioning,,,
+gcc, Using the GNU Compiler Collection (GCC)}).
+
 @item --with-source=@var{source}
 @itemx --with-source=@var{package}=@var{source}
 @itemx --with-source=@var{package}@@@var{version}=@var{source}
diff --git a/guix/transformations.scm b/guix/transformations.scm
index 5ae1977cb2..3be02179ef 100644
--- a/guix/transformations.scm
+++ b/guix/transformations.scm
@@ -29,6 +29,7 @@  (define-module (guix transformations)
   #:autoload   (guix upstream) (package-latest-release
                                 upstream-source-version
                                 upstream-source-signature-urls)
+  #:autoload   (guix cpu) (current-cpu cpu->gcc-architecture)
   #:use-module (guix utils)
   #:use-module (guix memoization)
   #:use-module (guix gexp)
@@ -49,6 +50,9 @@  (define-module (guix transformations)
   #:export (options->transformation
             manifest-entry-with-transformations
 
+            tunable-package?
+            tuned-package
+
             show-transformation-options-help
             %transformation-options))
 
@@ -419,6 +423,120 @@  (define replacements
             obj)
         obj)))
 
+(define tuning-compiler
+  (mlambda (micro-architecture)
+    "Return a compiler wrapper that passes '-march=MICRO-ARCHITECTURE' to the
+actual compiler."
+    (define wrapper
+      #~(begin
+          (use-modules (ice-9 match))
+
+          (define* (search-next command
+                                #:optional
+                                (path (string-split (getenv "PATH")
+                                                    #\:)))
+            ;; Search the next COMMAND on PATH, a list of
+            ;; directories representing the executable search path.
+            (define this
+              (stat (car (command-line))))
+
+            (let loop ((path path))
+              (match path
+                (()
+                 (match command
+                   ("cc" (search-next "gcc"))
+                   (_ #f)))
+                ((directory rest ...)
+                 (let* ((file (string-append
+                               directory "/" command))
+                        (st   (stat file #f)))
+                   (if (and st (not (equal? this st)))
+                       file
+                       (loop rest)))))))
+
+          (match (command-line)
+            ((command arguments ...)
+             (match (search-next (basename command))
+               (#f (exit 127))
+               (next
+                (apply execl next
+                       (append (cons next arguments)
+                           (list (string-append "-march="
+                                                #$micro-architecture))))))))))
+
+    (define program
+      (program-file (string-append "tuning-compiler-wrapper-" micro-architecture)
+                    wrapper))
+
+    (computed-file (string-append "tuning-compiler-" micro-architecture)
+                   (with-imported-modules '((guix build utils))
+                     #~(begin
+                         (use-modules (guix build utils))
+
+                         (define bin (string-append #$output "/bin"))
+                         (mkdir-p bin)
+
+                         (for-each (lambda (program)
+                                     (symlink #$program
+                                              (string-append bin "/" program)))
+                                   '("cc" "gcc" "clang" "g++" "c++" "clang++")))))))
+
+(define (tuned-package p micro-architecture)
+  "Return package P tuned for MICRO-ARCHITECTURE."
+  (define compiler
+    (tuning-compiler micro-architecture))
+
+  (package
+    (inherit p)
+    (native-inputs
+     ;; Arrange so that COMPILER comes first in $PATH.
+     `(("tuning-compiler" ,compiler)
+       ,@(package-native-inputs p)))
+    (arguments
+     (substitute-keyword-arguments (package-arguments p)
+       ((#:tests? _ #f) #f)))
+    (properties
+     `((cpu-tuning . ,micro-architecture)
+       ,@(package-properties p)))))
+
+(define (tunable-package? package)
+  "Return true if package PACKAGE is \"tunable\"--i.e., if tuning it for the
+host CPU is worthwhile."
+  (assq 'tunable? (package-properties package)))
+
+(define package-tuning
+  (mlambda (micro-architecture)
+    "Return a procedure that maps the given package to its counterpart tuned
+for MICRO-ARCHITECTURE, a string suitable for GCC's '-march'."
+    (define rewriting-property
+      (gensym " package-tuning"))
+
+    (package-mapping (lambda (p)
+                       (cond ((assq rewriting-property (package-properties p))
+                              p)
+                             ((assq 'tunable? (package-properties p))
+                              (package/inherit p
+                                (replacement (tuned-package p micro-architecture))
+                                (properties `((,rewriting-property . #t)
+                                              ,@(package-properties p)))))
+                             (else
+                              p)))
+                     (lambda (p)
+                       (assq rewriting-property (package-properties p)))
+                     #:deep? #t)))
+
+(define (transform-package-tuning micro-architectures)
+  "Return a procedure that, when "
+  (match micro-architectures
+    ((micro-architecture _ ...)
+     (info (G_ "tuning for CPU micro-architecture ~a~%")
+           micro-architecture)
+     (let ((rewrite (package-tuning micro-architecture)))
+       (lambda (obj)
+         (if (package? obj)
+             (rewrite obj)
+             obj))))))
+
 (define (transform-package-with-debug-info specs)
   "Return a procedure that, when passed a package, set its 'replacement' field
 to the same package but with #:strip-binaries? #f in its 'arguments' field."
@@ -601,6 +719,7 @@  (define %transformations
     (with-commit . ,transform-package-source-commit)
     (with-git-url . ,transform-package-source-git-url)
     (with-c-toolchain . ,transform-package-toolchain)
+    (tune . ,transform-package-tuning)
     (with-debug-info . ,transform-package-with-debug-info)
     (without-tests . ,transform-package-tests)
     (with-patch  . ,transform-package-patches)
@@ -640,6 +759,21 @@  (define %transformation-options
                   (parser 'with-git-url))
           (option '("with-c-toolchain") #t #f
                   (parser 'with-c-toolchain))
+          (option '("tune") #f #t
+                  (lambda (opt name arg result . rest)
+                    (define micro-architecture
+                      (match arg
+                        ((or #f "native")
+                         (cpu->gcc-architecture (current-cpu)))
+                        ("generic" #f)
+                        (_ arg)))
+
+                    (apply values
+                           (if micro-architecture
+                               (alist-cons 'tune micro-architecture
+                                           result)
+                               (alist-delete 'tune result))
+                           rest)))
           (option '("with-debug-info") #t #f
                   (parser 'with-debug-info))
           (option '("without-tests") #t #f
diff --git a/tests/transformations.scm b/tests/transformations.scm
index 09839dc1c5..760b523e6e 100644
--- a/tests/transformations.scm
+++ b/tests/transformations.scm
@@ -465,6 +465,26 @@  (define (package-name* obj)
                    `((with-latest . "foo")))))
           (package-version (t p)))))
 
+(test-equal "options->transformation, tune"
+  '(cpu-tuning . "superfast")
+  (let* ((p0 (dummy-package "p0"))
+         (p1 (dummy-package "p1"
+               (inputs `(("p0" ,p0)))
+               (properties '((tunable? . #t)))))
+         (p2 (dummy-package "p2"
+               (inputs `(("p1" ,p1)))))
+         (t  (options->transformation '((tune . "superfast"))))
+         (p3 (t p2)))
+    (and (not (package-replacement p3))
+         (match (package-inputs p3)
+           ((("p1" tuned))
+            (match (package-inputs tuned)
+              ((("p0" p0))
+               (and (not (package-replacement p0))
+                    (assq 'cpu-tuning
+                          (package-properties
+                           (package-replacement tuned)))))))))))
+
 (test-equal "options->transformation + package->manifest-entry"
   '((transformations . ((without-tests . "foo"))))
   (let* ((p (dummy-package "foo"))