Code Monkey home page Code Monkey logo

vulkan-memorymodel's Introduction

Vulkan-MemoryModel

The Vulkan-MemoryModel repo includes documentation and tools related to the Vulkan Memory Model that are not built into the core specification. The core specification is available at https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#VK_KHR_vulkan_memory_model. Other Khronos-maintained specifications and software projects that have been extended to support this model are listed in the extension release checklist.

Alloy Model

The alloy subdirectory includes an Alloy implementation of the memory model, a set of litmus tests written in a rudimentary syntax, C++ source for a tool to translate the litmus tests to alloy, and a makefile to execute the tests. A java class to run the alloy tests is compiled against https://oss.sonatype.org/content/repositories/snapshots/org/alloytools/org.alloytools.alloy.dist/5.0.0-SNAPSHOT/org.alloytools.alloy.dist-5.0.0-20190619.101010-34.jar. Download this jar file and place it in the alloy subdirectory. Simply run "make -j4" from the alloy subdirectory to run the tests. Required dependencies are just g++, GNU make, alloy, and a Java runtime.

License

This repo is treated as an offshoot of the Vulkan-Docs repo, and uses the same Creative Commons Attribution 4.0 license. See COPYING.md.

Alloy is a third-party tool used in this repo, and is available from https://github.com/AlloyTools/org.alloytools.alloy.

vulkan-memorymodel's People

Contributors

jeffbolznv avatar outofcontrol avatar raphlinus avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

vulkan-memorymodel's Issues

Redundant use of storage[CBAR] in the last line of the definition of synchronizes-with

This last line reads:
((stor[REL&F]) . (rc[po]) . (stor[CBAR]) . (scbarinst & inscope - iden) . (stor[CBAR]) . (rc[po]) . (stor[ACQ&F])))
Because scbarinst is guaranteed to be between CBARs (through the line 223: "is_equivalence[scbarinst, CBAR]"), I believe both (store[CBAR]) can be removed without changing the meaning of this line, turning it into:
((stor[REL&F]) . (rc[po]) . (scbarinst & inscope - iden) . (rc[po]) . (stor[ACQ&F])))

consistent[X] is too strict

The current formulation of consistent[X] rejects a class of execution traces involving nonatomic races. This, I believe, prevents clean expression of “failing” litmus test cases, and prevents correct interpretation of one program I’ve found. There is a simple fix, which behaves much more like I’d expect and has manageable impact on the existing test suite.

I’ll develop my argument through three simple programs. The first two represent trivial fail and pass cases, respectively, and the third is one that distinguishes memory models. It cannot be expressed in C11, it is accepted in PTX, and, I believe, is rejected by the Vulkan Memory Model, though capturing that in a test is tricky.

Trivial fail: nonatomic race

This is a case where one thread writes a value and another reads it. This is obviously a data race and should be rejected, but expressing that in a test is subtle.

NEWTHREAD st.scopedev.sc0 x = 1 NEWTHREAD ld.scopedev.sc0 x

There are two executions, one where rf is empty (so #RFINIT = 1), and the other where rf is a single edge, let’s call it W1 -> R1. The first is fine - it is consistent because rf is empty, and dr contains R1 <-> W1 because there is no locord relation.

However, the second is problematic. Here, consistent[X] fails because rf is not contained in imm([W]; locord); the latter is empty because there are no locord relations. I believe this is wrong; this execution should be considered consistent, but should of course still be considered a failure because of the data race.

Note this program can be expressed and analyzed in terms of the Wickerson et al 2017 formalization of the C11 memory model. There, the second execution is also not considered consistent (see (30), the second line of consistent_{C11}, shown in Figure 6). I conjecture that this is a minor flaw, because while this formalization does not reject this specific execution as racy, it still correctly rejects the program because of the first execution.

Trivial success: atomic order

Note: this test is the first three threads of asmo.test.

NEWTHREAD st.atom.scopedev.sc0 x = 1 NEWTHREAD st.atom.scopedev.sc0 x = 2 NEWTHREAD ld.atom.scopedev.sc0 x = 1 ld.atom.scopedev.sc0 x = 2

Again, there are two executions, in this case corresponding to two values of asmo: W1 -> W2 and W2->W1. The first is consistent, the second is inconsistent, not because of anything that went wrong, but merely because it doesn’t match the observation in the third thread.

The lesson from this example is that the existence of an inconsistent solution is not a problem. This should be considered a trivially passing test.

Tricky case: mixed atomics and nonatomics

This one is a little more complicated to express, because there is control flow in the source program. Is it a data race or not?

thread1() {
    atomicStore(x, 1);
}

thread2() {
    if (atomicLoad(x) == 1) {
        read(x);
    }
}

This maps to two traces, depending on the predicate. First, false:

NEWTHREAD
st.atom.scopedev.sc0 x = 1
NEWTHREAD
ld.atom.scopedev.sc0 x = 0

This is consistent and has no data races. The nonatomic load is not invoked because the predicate is false.

Now the trace where the predicate is true:

NEWTHREAD
st.atom.scopedev.sc0 x = 1
NEWTHREAD
ld.atom.scopedev.sc0 x = 1
ld.scopedev.sc0 x

There are two executions, corresponding to the nonatomic read being RFINIT, or reading from the atomic write. In the current state, neither is consistent, for different reasons. The first execution is caught by the acyclic test; there is a from-read (fr) from the nonatomic read to the write, which forms a cycle with the read-from (rf) the atomic write to the atomic read and the locord form the atomic read to the nonatomic read. This is arguably correctly classified as inconsistent, though it’s certainly possible to imagine a sufficiently weak implementation that the nonatomic read may read 0. The second execution is (incorrectly) classified as inconsistent for the same reason as the plain data race above.

This program cannot be expressed in the C11 memory model because it mixes atomic and nonatomic access to the same location. Therefore it is not meaningful to ask whether the Wickerson formalization will accept or reject it.

It is accepted by the PTX memory model (it is basically the CoRR test in Figure 9a, which guarantees the nonatomic load will see 1 if the preceding atomic load did).

How to interpret tests

A generic passing test (mp.test is a good example) should be:

SATISFIABLE consistent[X] && #dr = 0
NOSOLUTION consistent[X] && #dr > 0

The first line is the same as the current state. Informally, it says, “there exists a consistent execution with no races.” The second line means, “all consistent executions are race-free.” Note that it doesn’t invoke #RFINIT as in the current formulation of the test. I consider that a workaround rather than a principled expression of the intent of the test.

There are two notions of failing tests. One constrains the implementation (asmo.test is an example) and is just NOSOLUTION consistent[X]. This is fine. The other constrains valid programs (eg test16.test) and needs more work. I believe it should read:

SATISFIABLE consistent[X] && #dr > 0

If there are multiple executions, some of which are racy, some not, I think it should be considered a failing test, therefore I don’t believe there’s any need for a second NOSOLUTION line.

The fix

I believe the second clause in consistent[X] should be changed to:

  no (X.rf . (stor[X.R - X.A])) & (stor[X.W] . (X.locord) . ^(stor[X.W] . (X. locord)))

It might be worth defining a helper function for z.^z, meaning transitive chains of at least two, which would simplify this to:

  no (X.rf . (stor[X.R - X.A])) & two_or_more[stor[X.W] . (X.locord)]

I’ll try to explain the change informally. The current state (expanding the definition of imm) says “an atomic read is from a write ordered by locord, and is not shadowed by another write ordered by locord.” My proposed change drops the first conjunct and keeps the second. For single-threaded programs it is the same, as all (nonatomic) memory events participate in locord, but for racy multithreaded cases, it expands the set of consistent executions considerably to include those races.

For reference of how PTX analyzes nonatomic consistency in single-threaded programs, see Axiom 5 (section 3.5.5 in the paper), where it is captured by an acyclic condition on rf, fr, and program order. A read from a shadowed write in a single-threaded context (forbidden by my proposed change) would induce such a cycle, so one direction of an implication is established. Whether there are other cycles that violate causality that would not be caught by my proposed rule is an open question. If not, then in practice the set of executions considered consistent in Vulkan and PTX should be equivalent (modulo of course the differences in programs that can be expressed).

Summary

The Wickerson formalization on which the Vulkan Memory model is based has a minor flaw of misclassifying some racy executions as inconsistent. It's not clear this is a serious problem, as all cases I've looked at correctly classify the program as consistent but racy. However, when mixing atomics and nonatomics that assumption breaks down, and I've given an example of a program which I believe is misclassified (all consistent executions are race-free, but the racy executions are classified as inconsistent).

A relatively small change to the text fixes this, classifying this sort of racy program as consistent. Thus, the "pass" and "fail" conditions for tests can be written more cleanly, in particular not invoking RFINIT. I believe it affects the formalization and not the spec text, though it may be that on review there is some language that could be clarified.

I'm happy to provide more detailed litmus tests and Alloy graphs if they're needed, though I hope this presentation is adequate.

References

Wickerson et al 2017: Automatically Comparing Memory Consistency Models

PTX: A Formal Analysis of the NVIDIA PTX Memory Consistency Model

Typo in the definition of ithbsemsc1

This definition makes a reference to SC0 instead of SC1 on the line 312:
(stor[SC0+SEMSC1]).po.(stor[REL&SEMSC1]) +
I believe it should be
(stor[SC1+SEMSC1]).po.(stor[REL&SEMSC1]) +
instead.

How to interpret data race expected results

I am trying to understand how I should interpret the expected results related to data races in the litmus tests. Consider the example below.

NEWWG
NEWSG
NEWTHREAD
st.av.scopedev.sc0 x = 1
st.atom.rel.scopewg.sc1.semsc0 y = 1
NEWSG
NEWTHREAD
ld.atom.acq.scopewg.sc1.semsc0 y = 1
ld.vis.scopedev.sc0 x
SATISFIABLE consistent[X] && #dr=0
NOSOLUTION consistent[X] && #dr>0

Which of the following interpretations is correct?

  • "the program does not contain any racy execution", or
  • "the execution in which the second thread observes y=1 is not racy"

Put it differently, do the values enforced in the syntax always filter executions? This makes sense to enforce a safety/reachability condition, but I would expect that while reasoning about data races, one cares about all executions.

Redundant use of sloc in locord

In the definition of locord, line 326 reads:
((stor[R-PRIV]) . (hb & sloc) . (stor[R+W-PRIV])) +
I believe that the sloc here is redundant, as there is already a reference to sloc on line 323:
((R+W)->(R+W)) & sloc &
that provides the exact same guarantee. So it should be safe to replace line 326 by:
((stor[R-PRIV]) . hb . (stor[R+W-PRIV])) +

Relation between sref and sloc

I am trying to understand if the property sref <= sloc holds (where <= means subset of) and thus having sloc & sref in the definition of mutordatom is redundant.

If I think of sref and sloc as virtual an physical memory, I think the property should hold. However, it seems that on the device, sref can be other things. Thus, I'm not completely sure if the property should also hold for those cases.

RMW atomicity is broken

We are getting some counter intuitive behaviors related to non-atomic accesses and rmw.
Consider this example

// Both read-modify-write instructions read the value stored by a non-atomic write instruction preceding the barrier.
NEWWG
NEWSG
NEWTHREAD
st.av.scopewg.sc1 x = 1
cbar.acq.rel.scopewg.semsc1 0
rmw.scopewg.sc1 x = 1 2
NEWSG
NEWTHREAD
cbar.acq.rel.scopewg.semsc1 0
rmw.scopewg.sc1 x = 1 2
SATISFIABLE consistent[X] && #dr=0
NOSOLUTION consistent[X] && #dr>0

This clearly violates atomicity, but the current model allows the behavior.

We have tried to add more semantics, but it does not help

NEWWG
NEWSG
NEWTHREAD
st.av.scopewg.sc1 x = 1
cbar.acq.rel.scopewg.semsc1.semav.semvis 0
rmw.acq.rel.scopewg.sc1.semsc1.semav.semvis x = 1 2
NEWSG
NEWTHREAD
cbar.acq.rel.scopewg.semsc1.semav.semvis 0
rmw.acq.rel.scopewg.sc1.semsc1.semav.semvis x = 1 2
SATISFIABLE consistent[X] && #dr=0
NOSOLUTION consistent[X] && #dr>0

If we make the store an atomic one, the behavior is forbidden.

@jeffbolznv any thoughts?

Add a note in the spec about causality cycles/out-of-thin air reads

This memory model appears to have inherited from C++ the issue of allowing causality cycles (a.k.a. out-of-thin-air reads). For example, the following program (in pseudo-code):

Thread 1:
  r = load_relaxed(x);
  store_relaxed(y, r);
Thread 2:
  r2 = load_relaxed(y);
  store_relaxed(x, r2);

can end with any value in both x and y. This is both unwanted (I don't know of any hardware that can actually write 42 in both x and y, with no reference to 42 anywhere in the program) and rather fundamental to the C++ style of axiomatic models. Fixing this in the model is way out-of-scope (it is a hard problem that academia has been trying to fix for several years, and the only solution I consider satisfactory rewrites the entire model in a different style), but we could include a comment similar to that in the C++ spec (note 9 in section 29.3 of http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf):

9 Implementations should ensure that no “out-of-thin-air” values are computed that circularly depend on their own computation.
[ Note: For example, with x and y initially zero,
// Thread 1:
r1 = y.load(memory_order_relaxed);
x.store(r1, memory_order_relaxed);
// Thread 2:
r2 = x.load(memory_order_relaxed);
y.store(r2, memory_order_relaxed);
should not produce r1 == r2 == 42, since the store of 42 to y is only possible if the store to x stores 42, which circularly depends on the store to y storing 42. Note that without this restriction, such an execution ispossible. —endnote]

A is in NONPRIV

The spec says: "Atomic operations are always considered non-private."
I did not see any translation of that sentence in the formal model, and it would be more convenient to have all such rules in one place. So I suggest adding
A in NONPRIV
just below NONPRIV in R+W

Does scoped modification order relate reads?

All I can find in the specification is:

Scoped Modification Order
For a given atomic operation A, all atomic operations that are mutually-ordered with A occur in an order known as A’s scoped modification order. A’s scoped modification order relates no other operations.

which hints that it includes reads.

But the formal model has a part saying:

// A's scoped modification order:
// - must be a strict partial order
// - must relate all mutually ordered atomic writes at the same location
strict_partial_order[asmo]
rai[asmo] = ((A&W) -> (A&W)) & mutordatom

From the name (modification order) and the details of the Scoped Modification Order Coherence section, I suspect the formal model is right here, and it is just an imprecision in the specification. Can you confirm this?

Release sequences headed by relaxed stores

Release sequences are used in 4 places in the spec, all 4 of them within the definition of synchronizes-with. In two of these places, release-sequences start from a release store, in the other two they start from an atomic write X with any memory semantics, with the comment that "If X is relaxed, it is still considered to head a hypothetical release sequence for this rule".

In the formal alloy model, release-sequences must start by a release write:
rs = (stor[REL&A]) . *((imm[asmo]) . (stor[R&W]))
and in the two cases where they are supposed to be able to start from any atomic write, "rs" is replaced by "(stor[A&W]) . (rc[rs])".
The problem is that in the case where the store is not release, rc[rs] is equivalent to iden.
Is it a problem with the spec (in which case the sentence "If X is relaxed, it is still considered to head a hypothetical release sequence for this rule" is strongly misleading), or with the alloy formalization?

If it is a problem with the formalization, I would remove the requirement to start with a release store from rs:
rs = *((imm[asmo]) . (stor[R&W]))
and just replace rc[rs] by rs in the two places it occurs in synchronizes-with.
This should be enough as synchronizes-with already makes explicit the requirements on the head of the release sequence (being a release atomic, or being an atomic write of arbitrary semantics).

Discrepancy in the coherency/consistency predicates

The specification has the following section:

Scoped Modification Order Coherence
Let A and B be mutually-ordered atomic operations, where A happens-before B, and let O be A’s scoped modification order. Then:

  • If A and B are both writes, then A must be earlier than B in O
  • If A and B are both reads, then the write that A takes its value from must be earlier in O than (or the same as) the write that B takes its value from
  • If A is a write and B is a read, then B must take its value from A or a write later than A in O
  • If A is a read and B is a write, then A must take its value from a write earlier than B in O

while the formal model has the following:

// consistency: locord, rf, fr, asmo must not form cycles
is_acyclic[X.locord + X.rf + X.fr + X.asmo]

These two things appear to be intended to match each other, but it is not obvious at all that they do.

I actually believe that they don't, in that the consistency predicate seems significantly stronger, especially considering that hb is not transitive in this model (contrary to C11 without consume whose formalization in
https://johnwickerson.github.io/papers/memalloy.pdf inspired this rule) so preventing cycles in hb + asmo is for example stronger than the first bullet in the spec. However, I have not yet managed to find a litmus test that makes the difference between the two clear. I plan on updating this issue once I find one. I am posting now as suggested in #1 in case anyone has some insight in this problem.

[RFC] Formal model written in cat and tool support for simulation

Together with @hernanponcedeleon, we wrote the vulkan model in the cat language (a standard language to formalize memory models), ported all the tests in this repository to a more standardized format, and added tool support for simulation/verification.

We believe the tool would be valuable for the community and we wanted to get feedback if it would make sense to have the model and the tests in this repository.

The new tool has the following advantages

  • it supports goto instructions which allow to write more realistic examples e.g. the XF-barrier.
  • it also supports the Nvidia's PTX model, meaning that it can be used to compare how the same program (modulo ISA syntax) behaves according to the different notions of consistency.
  • it scales much better than Alloy-based tools. For litmus tests scalability might not be an issue, but it could be used to verify more realistic code.
  • it allows to find certain liveness violations due to spinloops not synchronizing correctly.

avvisinc is redundant in the last two lines of the definition of locord

This is not a bug, but a proposed simplification.

These lines include the following parts:

  • (stor[W]) . (hb & avvisinc) . avdv
  • visdv . (hb & avvisinc) . (stor[R])
    Where avdv = stor[AVDEVICE] and visdv = stor[VISDEVICE]

We know that "SC0+SC1 = R+W"
, and avvisinc = (rai[((SC0+SC1)->(AVDEVICE+VISDEVICE)) + ...]) - iden + ...
, and no (R+W)&(AVDEVICE+VISDEVICE) (so we don't fall in the '- iden' case)
So (stor[W]) . avdv in avvisinc, and visdv . (stor[R]) in avvisinc.
So I suggest replacing "(hb & avvisinc)" by "hb" in these two lines.
This in turn would allow us to remove (SC0+SC1)->(AVDEVICE+VISDEVICE) from avvisinc, since avvisinc no longer appears in a position where either end could be AVDEVICE or VISDEVICE.

"((R+W)->(R+W))" in the definition of locord is redundant

This is not a bug, just a proposed minor simplification.

There is already a "sloc" constraint on locord, and from "is_equivalence[sloc, R+W]" we know that sloc in ((R+W)->(R+W)). So it should be safe to remove the "((R+W)->(R+W)) &" part of locord.

Synchronizes-with appears to be missing an sref constraint

For example the first element of the definition is:
inscope & (
// atomic->atomic
((stor[REL&A]) . rs . (rf & inscope) . (stor[ACQ&A])) +
The corresponding part of the spec says:
If A and B are atomic operations, then A synchronizes-with B if and only if all of the following are true:
A performs a release operation
B performs an acquire operation
A and B are mutually-ordered
B reads a value written by A or by an operation in the release sequence headed by A
So the formalization appears to have replaced mutually-ordered by inscope. But mutually ordered does not only implies inscope, it also implies sref, and I do not where it is in the formal rule. The same thing is true of the other cases in the definition of synchronizes-with.

Am I missing something, or should this element be replaced by the following?
inscope & (
// atomic->atomic
((store[REL&A]) . rs . (rf & mutordatom) . (stor[ACQ&A])) +
Or alternatively:
inscope & (
// atomic->atomic
((store[REL&A]) . rs . (rf & inscope + sref) . (stor[ACQ&A])) +

"- (sthd & ref)" in the definition of datarace can be replaced by "- iden"

This is not a bug in the specification, just a proposed small simplification.

Currently data races are defined by
dr = sloc & ((W -> W) + (W -> R) + (R -> W) - mutordatom - (sthd & sref) - rai[locord])

Consider a pair (a, b) which would be in dr if not for "- (sthd & sref)".
If a == b, it will also be prevented by -iden
If a != b, then either a <po b or b <po a, from the axiom "(sthd - iden) = rai[po]"
So a <hb b or b <hb a
Since (a, b) is in sthd&sloc&sref, either a <locord b or b <locord a
So (a, b) would be prevented by rai[locord].

Synchronization using control barriers

I am trying to understand why the following test fails (i.e. I get SATISFIABLE)

NEWWG
NEWSG
NEWTHREAD
ld.scopedev.sc0 x = 1
cbar.scopewg 0
NEWTHREAD
cbar.scopewg 0
st.scopedev.sc0 x = 1
UNSATISFIABLE consistent[X]

My understanding is that both threads should synchronize at the control barrier, the load should then be ordered before the store, and thus it should not be able to observe value 1.

I tried playing around with making the memory accesses atomic and adding rel_acq semantics to the barriers, but I still get the same result. Am I missing something?

Does visible-to apply to atomic reads?

The spec just says:
If X is visible-to Y, then Y reads the value written by X for locations M2.
But the formal model has (in the predicate "consistent[X]"):
// consistency: non-atomic reads must read-from a value that is still visible
X.rf . (stor[X.R-X.A]) in imm[(stor[X.W]) . (X.locord)]
which restricts this rule to only non-atomic reads.

Is there a reason for the discrepancy?

On a related note, the formal model defines:
// visible to = location ordered W->R with no intervening write (W->W->R)
visto = imm[(stor[W]) . locord] . (stor[R])
But then never uses it (except in AssertVisTo).
Why not use it in the consistency predicate above?

Please rename default branch from 'master' to 'main' per Khronos policy

To repository owners: please rename the default branch of this repository from 'master' to 'main', using the Github renaming tool. This request is per policy set by the Khronos Promoters in May 2021, to follow Github community practice for respectful naming.

The Github renaming tool sets up URL redirections and retargets outstanding pull requests, so the impact on repository users is minimal. The most visible issue is that people with local repo clones will probably want to rename their clone's 'master' branch, following the popup instructions that will be seen when browsing the github repository after the change; or just delete 'master' and pull the new 'main', if it's purely a tracker with no local content.

You may wish to coordinate with @outofcontrol if you are doing auto-updates from this repository to another location, whether via push/pull mirroring or other mechanisms. The redirects setup by Github should accommodate most such uses transparently, but it's still good practice.

Based on experience with other KhronosGroup repositories which have undergone this renaming already, this is a reasonable approach:

  • Agree on a date for the renaming within the Working Group or other owners of the repository, and document that date here.
    • Date can be adjusted to avoid interaction with major releases in flight.
  • Provide notice to repository users outside Khronos, insofar as possible (adding a comment in the repo README with the switchover date is one way).
  • Once the renaming is done, edit the new 'main' branch to replace hardwired references to 'master' with (preferably) 'default branch' or 'main'.
  • Update the repo README to note the change.

If you have questions or issues about this, please raise them on Khronos internal gitlab 'khronos-general' issue 106 if possible. If not possible, you can @-tag me here.

While we will not force any WG into acting precipitously, this is our agreed policy. Please try to accommodate renaming relatively soon.

Note that this issue is automatically generated, due to the large number of KhronosGroup repositories it's being raised in.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.