Skip to content

Commit 67937c2

Browse files
committed
Add an opt-in mechanism for non-differentiable types.
When clad has no custom derivative for a called function it synthesizes one by cloning the function's body. For a type whose internals are not meant to be differentiated -- a standard-library primitive, an I/O stream -- that clone is at best wasteful and at worst ill-formed: cloning std::string's char-pointer constructor emits a static reverse-forward propagator that dereferences a null `this` and crashes CodeGen. A type is now non-differentiable when it carries the `non_differentiable` annotation -- on the type itself for code the user owns (CLAD_NONDIFFERENTIABLE), or on a clad::Tag<T> specialization for a foreign type the user cannot annotate at its declaration (CLAD_NONDIFFERENTIABLE_TYPE). utils::isNonDifferentiableType detects it, so clad never clones such a type's constructor or member bodies, and callOperatesOnNonDifferentiableType extends the treatment to a call whose object is opaque (e.g. a std::ostream operation). Seed std::string, std::allocator and the stream family in STLBuiltins.h. -fclad-porting-hints prints the paste-able CLAD_NONDIFFERENTIABLE_TYPE spelling and, for a reverse-forward pass, the elidable_reverse_forw route with its signature -- pointing at existing mechanisms rather than inventing new ones. isElidableConstructor is renamed to constructorReverseForwIsElidable and documented so its "a trivial copy is a shallow share" coverage is discoverable; that made a separate data-less-copy heuristic unnecessary. The user guide documents both macros; tests cover marker-based construction opacity (StringConstructor) and a marked-type call (NonDifferentiableMarkedType).
1 parent 4b34541 commit 67937c2

17 files changed

Lines changed: 469 additions & 56 deletions

docs/userDocs/source/user/CustomDerivatives.rst

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -467,21 +467,25 @@ to provide *and* the marker to declare instead:
467467
definition, descending into library internals
468468
note: to differentiate it, provide clad::custom_derivatives::scale_pullback
469469
with signature 'void (const Widget *, double, double, Widget *, double *)'
470-
note: or declare it non-differentiable with
471-
clad::custom_derivatives::nondifferentiable(clad::Tag<Widget>{})
470+
note: or mark it non-differentiable with CLAD_NONDIFFERENTIABLE_TYPE(Widget)
472471
473-
Each remark gives you the two ways to resolve the boundary:
472+
Each remark gives you the ways to resolve the boundary:
474473

475474
- **Differentiate it semantically.** Copy the printed signature and implement
476475
the custom derivative (a pushforward, pullback, or reverse-forward -- see the
477476
sections above). This is the right choice when the function has a meaningful
478477
derivative that is simpler or more correct than clad cloning its
479478
implementation (a matrix product's adjoint, a container's element access, ...).
480479
- **Mark it non-differentiable.** If the type carries no differentiable data
481-
(a stream, an allocator, a reference-count handle, ...), declare
482-
:code:`clad::custom_derivatives::nondifferentiable(clad::Tag<T>{})` and clad
483-
will treat every use of it as opaque. The marker note is emitted for member
484-
functions and constructors, where the enclosing type is the thing to mark.
480+
(a stream, an allocator, a reference-count handle, ...), mark it with
481+
:code:`CLAD_NONDIFFERENTIABLE_TYPE(T)` (see :doc:`UsingClad`) and clad will
482+
treat every construction of and call on it as opaque. The marker note is
483+
emitted for member functions and constructors, where the enclosing type is
484+
the thing to mark.
485+
- **Elide its reverse-forward pass.** For a reverse-forward-pass boundary whose
486+
pass is a no-op (e.g. a shallow copy that shares its adjoint), declare the
487+
printed :code:`..._reverse_forw` and mark it :code:`elidable_reverse_forw`;
488+
clad then skips the call instead of cloning a body.
485489

486490
Only functions outside the main file are reported, so differentiating your own
487491
code stays quiet; the remarks focus on the library edge you are porting. The

docs/userDocs/source/user/UsingClad.rst

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -598,36 +598,65 @@ The ``non_differentiable`` Attribute
598598

599599
Occasionally, you may want to skip differentiating a specific variable or function call. For example, some variables might be used purely for logging, as constants, or as standalone metrics. Clad provides the ``non_differentiable`` annotation attribute to safely omit generating derivatives for these components.
600600

601-
You can apply this attribute using Clang's annotation syntax. The most common approach is to define a macro alias:
601+
Clad ships the ``CLAD_NONDIFFERENTIABLE`` macro (defined in
602+
``clad/Differentiator/BuiltinDerivatives.h``, pulled in by ``Differentiator.h``)
603+
for this; it expands to ``__attribute__((annotate("non_differentiable")))``.
602604

603-
.. code-block:: c++
604-
605-
#define non_differentiable __attribute__((annotate("non_differentiable")))
606-
607-
If the ``non_differentiable`` attribute is applied to a variable, Clad skips generating a derivative counterpart for it:
605+
If ``CLAD_NONDIFFERENTIABLE`` is applied to a variable, Clad skips generating a
606+
derivative counterpart for it:
608607

609608
.. code-block:: c++
610609

611610
class PointData {
612611
public:
613612
double x;
614613
double y;
615-
non_differentiable double weight; // Clad will not compute derivatives with respect to this member
614+
CLAD_NONDIFFERENTIABLE double weight; // not differentiated
616615
};
617616

618617
If the attribute is applied to a function declaration, Clad refrains from producing any derivative expressions for that specific function. Instead, calls to the primal function are injected directly, behaving as if the result has a zero derivative:
619618

620619
.. code-block:: c++
621620

622-
non_differentiable double get_scaling_factor(double i, double j) {
623-
return i * j;
621+
CLAD_NONDIFFERENTIABLE double get_scaling_factor(double i, double j) {
622+
return i * j;
624623
}
625624

626-
double compute(double i, double j) {
627-
// get_scaling_factor will skip differentiation completely.
628-
return get_scaling_factor(i, j) + i * j;
625+
double compute(double i, double j) {
626+
// get_scaling_factor will skip differentiation completely.
627+
return get_scaling_factor(i, j) + i * j;
628+
}
629+
630+
Marking a type you do not own
631+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
632+
633+
The attribute must sit on the declaration you want to mark, so it cannot be
634+
attached to a library type declared in a header you do not control (a stream, an
635+
allocator, a third-party handle). For those, mark the type from the outside with
636+
``CLAD_NONDIFFERENTIABLE_TYPE`` at global scope; Clad then treats every
637+
construction of and call on that type as opaque:
638+
639+
.. code-block:: c++
640+
641+
CLAD_NONDIFFERENTIABLE_TYPE(ThirdParty::Handle);
642+
// template arguments (and commas) are fine:
643+
CLAD_NONDIFFERENTIABLE_TYPE(std::map<int, double>);
644+
645+
It expands to a ``clad::Tag`` specialization carrying
646+
``CLAD_NONDIFFERENTIABLE``. A concrete type is marked as written; a *template
647+
family* (e.g. every ``Boxed<T>``) needs a partial specialization the macro
648+
cannot express -- write it directly:
649+
650+
.. code-block:: c++
651+
652+
namespace clad {
653+
template <class T> class CLAD_NONDIFFERENTIABLE Tag<Boxed<T>> {};
629654
}
630655

656+
The standard-library primitives Clad already knows are non-differentiable
657+
(``std::string``, ``std::allocator``, the stream family) are marked this way in
658+
``STLBuiltins.h``.
659+
631660
Specifying Custom Derivatives
632661
-------------------------------
633662

include/clad/Differentiator/BuiltinDerivatives.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,21 @@ template <typename T, typename U> struct ValueAndAdjoint {
6060
/// We do the same for constructor_reverse_forw.
6161
template <class T> class Tag {};
6262

63+
/// Marks an entity non-differentiable: clad treats it as opaque and never
64+
/// clones its body to synthesize a derivative. Apply at the declaration of a
65+
/// variable, member, function, or type you own, e.g.
66+
/// struct CLAD_NONDIFFERENTIABLE Handle { double* data; };
67+
#define CLAD_NONDIFFERENTIABLE __attribute__((annotate("non_differentiable")))
68+
69+
/// Marks a type you do NOT own -- a library type you cannot annotate at its own
70+
/// declaration -- non-differentiable, by specializing clad::Tag for it. Use at
71+
/// global scope. The type may contain commas, e.g.
72+
/// CLAD_NONDIFFERENTIABLE_TYPE(std::map<int, double>);
73+
#define CLAD_NONDIFFERENTIABLE_TYPE(...) \
74+
namespace clad { \
75+
template <> class CLAD_NONDIFFERENTIABLE Tag<__VA_ARGS__> {}; \
76+
}
77+
6378
/// We have aliases with for old tags for backwards compatibility.
6479
template <class T> using ConstructorPushforwardTag = Tag<T>;
6580

include/clad/Differentiator/CladUtils.h

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,26 @@ namespace clad {
381381

382382
bool hasNonDifferentiableAttribute(const clang::Expr* E);
383383

384+
/// Returns true if \p RD is marked non-differentiable (opaque) by a
385+
/// clad::custom_derivatives::nondifferentiable(clad::Tag<T>) declaration:
386+
/// clad must not clone its member bodies to synthesize a derivative. The
387+
/// built-in standard-library markers live in STLBuiltins.h; users extend
388+
/// the set by declaring their own.
389+
bool isNonDifferentiableType(clang::Sema& S,
390+
const clang::CXXRecordDecl* RD);
391+
392+
/// Returns true if the type \p CE fundamentally operates on -- the object
393+
/// of a member call, or the first argument of a free operator (`os << x`)
394+
/// -- is non-differentiable (Tag-aware, so it honors clad::Tag<T> markers).
395+
/// Deliberately narrower than hasNonDifferentiableAttribute(Expr): it looks
396+
/// only at the operated-on type, so a differentiable call that merely
397+
/// passes a marked value is unaffected. Callers use it to skip the call
398+
/// outright (an early return) rather than to set the weaker nonDiff flag,
399+
/// which in reverse mode would still schedule a pullback and descend into
400+
/// the type's machinery.
401+
bool callOperatesOnNonDifferentiableType(clang::Sema& S,
402+
const clang::CallExpr* CE);
403+
384404
/// Collects every DeclRefExpr, MemberExpr, ArraySubscriptExpr in an
385405
/// assignment operator or a ternary if operator. This is useful to when we
386406
/// need to decide what needs to be stored on tape in reverse mode.
@@ -429,8 +449,17 @@ namespace clad {
429449
bool isLinearConstructor(const clang::CXXConstructorDecl* CD,
430450
const clang::ASTContext& C);
431451

432-
bool isElidableConstructor(const clang::CXXConstructorDecl* CD,
433-
const clang::ASTContext& C);
452+
/// Returns true if the reverse-forward propagator for constructor \p CD is
453+
/// structurally elidable -- clad need not synthesize one because the plain
454+
/// construction plus the normal member-wise adjoint handling already covers
455+
/// it. This holds for a trivial copy/move constructor (a shallow share of
456+
/// pointer/handle members), an aggregate, and a memberwise zero-or-copy
457+
/// initializer. A non-trivial constructor can additionally opt in with the
458+
/// elidable_reverse_forw attribute on its custom reverse_forw (see
459+
/// hasElidableReverseForwAttribute) -- both paths are combined where the
460+
/// propagator is consumed.
461+
bool constructorReverseForwIsElidable(const clang::CXXConstructorDecl* CD,
462+
const clang::ASTContext& C);
434463

435464
/// Returns true if T allows to edit any memory.
436465
bool isMemoryType(clang::QualType T);

include/clad/Differentiator/STLBuiltins.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
#include <clad/Differentiator/FunctionTraits.h>
88
#include <functional>
99
#include <initializer_list>
10+
#include <iosfwd>
1011
#include <iterator>
1112
#include <memory>
13+
#include <string>
1214
#include <tuple>
1315
#include <type_traits>
1416
#include <vector>
@@ -30,6 +32,30 @@ template <class T> void zero_init(typename std::allocator<T>&) {
3032
// do nothing, unclear if allocators have differentiable properties.
3133
}
3234

35+
// Mark a library type non-differentiable (opaque) by specializing clad::Tag for
36+
// it and annotating the specialization: the `non_differentiable` attribute
37+
// cannot be attached to a type declared in a header we do not own. clad then
38+
// treats the type as opaque and never clones its member bodies to synthesize a
39+
// derivative -- which for these primitives would be ill-formed. A custom
40+
// derivative, if present, still wins. Add your own non-differentiable library
41+
// types the same way.
42+
template <class C, class Tr, class A>
43+
class CLAD_NONDIFFERENTIABLE Tag<::std::basic_string<C, Tr, A>> {};
44+
template <class T> class CLAD_NONDIFFERENTIABLE Tag<::std::allocator<T>> {};
45+
// The stream family is non-differentiable I/O. Marking it lets clad treat a
46+
// stream operation (`os << x`, a streambuf method) as opaque instead of
47+
// descending into sentry/streambuf/scope_guard/char_traits machinery. Only
48+
// forward declarations (<iosfwd>) are needed to name the templates.
49+
template <class C, class Tr>
50+
class CLAD_NONDIFFERENTIABLE Tag<::std::basic_ostream<C, Tr>> {};
51+
template <class C, class Tr>
52+
class CLAD_NONDIFFERENTIABLE Tag<::std::basic_istream<C, Tr>> {};
53+
template <class C, class Tr>
54+
class CLAD_NONDIFFERENTIABLE Tag<::std::basic_streambuf<C, Tr>> {};
55+
template <class C, class Tr>
56+
class CLAD_NONDIFFERENTIABLE Tag<::std::basic_ios<C, Tr>> {};
57+
template <> class CLAD_NONDIFFERENTIABLE Tag<::std::ios_base> {};
58+
3359
namespace custom_derivatives {
3460

3561
namespace helpers {

lib/Differentiator/CladUtils.cpp

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,8 @@ namespace clad {
359359
return true;
360360
}
361361

362-
bool isElidableConstructor(const clang::CXXConstructorDecl* CD,
363-
const clang::ASTContext& C) {
362+
bool constructorReverseForwIsElidable(const clang::CXXConstructorDecl* CD,
363+
const clang::ASTContext& C) {
364364
const CXXRecordDecl* RD = CD->getParent();
365365
if (CD->isCopyOrMoveConstructor() && CD->isTrivial())
366366
return true;
@@ -689,6 +689,51 @@ namespace clad {
689689
return false;
690690
}
691691

692+
bool isNonDifferentiableType(clang::Sema& S,
693+
const clang::CXXRecordDecl* RD) {
694+
// A type is non-differentiable (opaque) when it carries the
695+
// `non_differentiable` annotation. For a type the user owns the attribute
696+
// sits on the type itself; for a foreign type it cannot, so the opt-out
697+
// is expressed on a clad::Tag<T> specialization instead (see
698+
// STLBuiltins.h). Either way clad never clones the type's member bodies
699+
// to synthesize a derivative, which for library internals is ill-formed
700+
// (see DiffCollector::VisitCXXConstructExpr). A custom derivative, if
701+
// present, still wins: it is looked up first, so the marker only
702+
// suppresses the body-clone fallback.
703+
if (!RD)
704+
return false;
705+
if (hasNonDifferentiableAttribute(RD))
706+
return true;
707+
// Out-of-line opt-out: the attribute lives on clad::Tag<RD>. Completing
708+
// the specialization instantiates a matching partial specialization's
709+
// attribute onto it (Tag's primary template is an empty class, so
710+
// completion never fails); then read the attribute off it.
711+
QualType TagTy = GetCladTagOfType(
712+
S, clad_compat::getRecordType(S.getASTContext(), RD));
713+
S.isCompleteType(GetValidSLoc(S), TagTy);
714+
const auto* TagRD = TagTy->getAsCXXRecordDecl();
715+
return TagRD && hasNonDifferentiableAttribute(TagRD);
716+
}
717+
718+
bool callOperatesOnNonDifferentiableType(clang::Sema& S,
719+
const clang::CallExpr* CE) {
720+
// Extract the type the call operates on -- the declaring class of a
721+
// member call, or the first argument of a free operator (`os << x`) --
722+
// and ask whether it is non-differentiable. See the header for why this
723+
// is narrower than hasNonDifferentiableAttribute(Expr) and why callers
724+
// skip on it rather than set the nonDiff flag.
725+
const clang::FunctionDecl* FD = CE->getDirectCallee();
726+
if (const auto* MD = dyn_cast_or_null<clang::CXXMethodDecl>(FD))
727+
if (isNonDifferentiableType(S, MD->getParent()))
728+
return true;
729+
if (FD && FD->isOverloadedOperator() && CE->getNumArgs() >= 1) {
730+
QualType T = CE->getArg(0)->getType().getNonReferenceType();
731+
if (isNonDifferentiableType(S, T->getAsCXXRecordDecl()))
732+
return true;
733+
}
734+
return false;
735+
}
736+
692737
bool hasElidableReverseForwAttribute(const clang::Decl* D) {
693738
for (auto* Attr : D->specific_attrs<clang::AnnotateAttr>())
694739
if (Attr->getAnnotation() == "elidable_reverse_forw")

lib/Differentiator/DerivativeBuilder.cpp

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -476,8 +476,12 @@ static void registerDerivative(Decl* D, Sema& S, const DiffRequest& R) {
476476
const FunctionDecl* FD = request.Function;
477477
// A custom derivative already covers this call; nothing to port. Only a
478478
// function whose *definition* clad is about to clone is a porting gap.
479+
// Constructors have a non-identifier name (a constructor name) but are a
480+
// primary porting case (e.g. std::string's constructor), so let them
481+
// through; other non-identifier names (operators) have no clean suggested
482+
// custom-derivative spelling, so skip them.
479483
if (!FD || request.CustomDerivative || !FD->isDefined() ||
480-
!FD->getDeclName().isIdentifier())
484+
(!FD->getDeclName().isIdentifier() && !isa<CXXConstructorDecl>(FD)))
481485
return;
482486
// Hint only at a library boundary: a function defined outside the main
483487
// source file (an included header), where the user decides "differentiate
@@ -502,11 +506,36 @@ static void registerDerivative(Decl* D, Sema& S, const DiffRequest& R) {
502506
"to differentiate it, provide clad::custom_derivatives::%0 with "
503507
"signature %1")
504508
<< request.ComputeDerivativeName() << DerivativeType;
505-
if (const auto* MD = dyn_cast<CXXMethodDecl>(FD))
509+
if (const auto* MD = dyn_cast<CXXMethodDecl>(FD)) {
510+
const CXXRecordDecl* RD = MD->getParent();
511+
// Print the qualified name WITH template arguments
512+
// (getQualifiedNameAsString drops them, yielding an ill-formed Tag<Boxed>
513+
// for a Boxed<double>).
514+
clang::PrintingPolicy Policy = m_Sema.getPrintingPolicy();
515+
Policy.SuppressTagKeyword = true;
516+
std::string TypeName =
517+
clad_compat::getRecordType(m_Context, RD).getAsString(Policy);
506518
diag(DiagnosticsEngine::Note, Loc,
507-
"or declare it non-differentiable with "
508-
"clad::custom_derivatives::nondifferentiable(clad::Tag<%0>{})")
509-
<< MD->getParent()->getQualifiedNameAsString();
519+
"or mark it non-differentiable with CLAD_NONDIFFERENTIABLE_TYPE(%0)")
520+
<< TypeName;
521+
// The value-level macro marks a single type; a template instantiation is
522+
// only that specialization. Marking the whole family needs a clad::Tag
523+
// partial specialization the macro cannot express.
524+
if (isa<clang::ClassTemplateSpecializationDecl>(RD))
525+
diag(DiagnosticsEngine::Note, Loc,
526+
"this marks only this specialization; to mark the whole template, "
527+
"add a clad::Tag partial specialization carrying "
528+
"CLAD_NONDIFFERENTIABLE");
529+
}
530+
// A reverse-forward pass that is a no-op (e.g. a shallow copy that shares
531+
// its adjoint) need not be cloned: declare it and mark it elidable so clad
532+
// skips the call. Point at the existing mechanism rather than a new one.
533+
if (request.Mode == DiffMode::reverse_mode_forward_pass)
534+
diag(DiagnosticsEngine::Note, Loc,
535+
"or, if its reverse-forward pass is a no-op (e.g. a shallow copy "
536+
"that shares its adjoint), declare %0 with signature %1 and mark it "
537+
"elidable_reverse_forw")
538+
<< request.ComputeDerivativeName() << DerivativeType;
510539
}
511540

512541
DerivativeAndOverload

lib/Differentiator/DiffPlanner.cpp

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,15 @@ static QualType GetDerivedFunctionType(const CallExpr* CE) {
12521252
if (clad::utils::hasNonDifferentiableAttribute(E))
12531253
nonDiff = true;
12541254

1255+
// A call whose object is a marked non-differentiable type -- a stream
1256+
// operation (`os << x`) or a std::streambuf method -- is opaque. Do not
1257+
// schedule a derivative for it or recurse into its body; otherwise clad
1258+
// descends into the whole non-differentiable I/O machinery
1259+
// (sentry/streambuf/scope_guard/char_traits). The primal call is emitted
1260+
// as-is in the derivative.
1261+
if (clad::utils::callOperatesOnNonDifferentiableType(m_Sema, E))
1262+
return true;
1263+
12551264
request.VerboseDiags = false;
12561265
request.EnableTBRAnalysis = m_TopMostReq->EnableTBRAnalysis;
12571266
request.EnableVariedAnalysis = m_TopMostReq->EnableVariedAnalysis;
@@ -1614,10 +1623,21 @@ static QualType GetDerivedFunctionType(const CallExpr* CE) {
16141623
forwPassRequest.BaseFunctionName = "constructor";
16151624
forwPassRequest.Mode = DiffMode::reverse_mode_forward_pass;
16161625
forwPassRequest.CallContext = E;
1626+
forwPassRequest.EmitPortingHints = m_TopMostReq->EmitPortingHints;
16171627
QualType recordTy = CD->getThisType()->getPointeeType();
16181628
bool elideRevForw =
1619-
utils::isElidableConstructor(CD, m_Sema.getASTContext());
1620-
if (LookupCustomDerivativeDecl(forwPassRequest) || !elideRevForw)
1629+
utils::constructorReverseForwIsElidable(CD, m_Sema.getASTContext());
1630+
// Cloning the body of a non-differentiable constructor yields an ill-formed
1631+
// reverse-forward propagator: e.g. std::string's char-pointer constructor
1632+
// (reached through a Kokkos::View label) delegates to a private member
1633+
// (this->__init(...)), but the propagator is a static function with no
1634+
// `this`, so CodeGen crashes. Build a propagator for a marked type only
1635+
// when the user supplied a custom derivative, never by cloning the library
1636+
// body.
1637+
bool cloneBodyUnsafe =
1638+
utils::isNonDifferentiableType(m_Sema, CD->getParent());
1639+
if (LookupCustomDerivativeDecl(forwPassRequest) ||
1640+
(!elideRevForw && !cloneBodyUnsafe))
16211641
m_DiffRequestGraph.addNode(forwPassRequest, /*isSource=*/true);
16221642

16231643
// Don't build propagators for calls that do not contribute in

0 commit comments

Comments
 (0)