//include
//------------------------------------------
#include <bits/stdc++.h>
using namespace std;

//typedef
//------------------------------------------
typedef vector<int> VI;
typedef vector<VI> VVI;
typedef vector<string> VS;
typedef pair<int, int> PII;
typedef long long LL;

//container util
//------------------------------------------
#define ALL(a)  (a).begin(),(a).end()
#define RALL(a) (a).rbegin(), (a).rend()
#define PB push_back
#define MP make_pair
#define SZ(a) int((a).size())
#define EACH(i,c) for(typeof((c).begin()) i=(c).begin(); i!=(c).end(); ++i)
#define EXIST(s,e) ((s).find(e)!=(s).end())
#define SORT(c) sort((c).begin(),(c).end())

//constant
//--------------------------------------------
const double EPS = 1e-10;
const double PI  = acos(-1.0);
const int MOD = 1000000007;
const int INF = 2147483647;

//debug
#define dump(x)  cerr << #x << " = " << (x) << endl;
#define debug(x) cerr << #x << " = " << (x) << " (L" << __LINE__ << ")" << " " << __FILE__ << endl;

//myfuncs
//素数判定
template<class T> inline T isPrime(T x) {
    bool ret = true;
    if (x < 2) {
        ret = false;
    }
    for (LL i = 2; i <= sqrt(x); i++) {
        if (x%i == 0) {
            ret = false;
        }
    }
    return ret;
}

//Union FInd
struct UnionFind {
    vector<int> par; // par[i]:iの親の番号　(例) par[3] = 2 : 3の親が2

    UnionFind(int N) : par(N) { //最初は全てが根であるとして初期化
        for(int i = 0; i < N; i++) par[i] = i;
    }

    int root(int x) { // データxが属する木の根を再帰で得る：root(x) = {xの木の根}
        if (par[x] == x) return x;
        return par[x] = root(par[x]);
    }

    void unite(int x, int y) { // xとyの木を併合
        int rx = root(x); //xの根をrx
        int ry = root(y); //yの根をry
        if (rx == ry) return; //xとyの根が同じ(=同じ木にある)時はそのまま
        par[rx] = ry; //xとyの根が同じでない(=同じ木にない)時：xの根rxをyの根ryにつける
    }

    bool same(int x, int y) { // 2つのデータx, yが属する木が同じならtrueを返す
        int rx = root(x);
        int ry = root(y);
        return rx == ry;
    }
};

//main code

const int NMAX = 100000;
// VVI x(NMAX,VI(NMAX,0));    10^5 * 10^5はオーバーフローっぽい
vector<vector<pair<int,int>>> x(NMAX);
vector<bool> visited(NMAX,false);
vector<int> sum_cost(NMAX,0);


void dfs(int v,int n){
    visited[v] = true;

    for (int i = 0; i < x[v].size(); i++) {
        int next_v = x[v][i].first;
        int cost = x[v][i].second;
        // すでにコストが入力されている時、トータルコストに矛盾が生じないかの検証
        if (sum_cost[next_v] != 0) {
            if (sum_cost[next_v] != sum_cost[v] + cost) {
                cout << "No" << endl;
                exit(0);
            }
        }
        // 矛盾がなければ探索続行
        else if (!visited[next_v]) {
            sum_cost[next_v] = sum_cost[v] + cost;
            dfs(x[v][i].first,n);
        }

    }
}


int main(int argc, char const* argv[])
{
    int n,m;
    cin >> n >> m;
    set<int> all_v;  // 複数のグラフに分かれた時に対応するため
    if (m == 0) {
        cout << "Yes" << endl;
        return 0;
    }
    int l,r,d;
    for (int i = 0; i < m; i++) {
        cin >> l >> r >> d;
        x[l-1].push_back(make_pair(r-1,d));
        x[r-1].push_back(make_pair(l-1,-d));
        all_v.insert(l-1);
        all_v.insert(r-1);
    }

    // 訪れる必要がある頂点に対して、まだ到達していなければdfs実行
    for (auto&& v : all_v) {
        if (!visited[v]) {
            dfs(v,n);
        } 
    }

    // 矛盾なくdfs抜けたらyes
    cout << "Yes" << endl;
    return 0;
}


Measured memory_bytes: 16896